HTTP & Transport
The EDK has two communication systems that serve different roles but share the same command-oriented architecture:
| System | Purpose | Protocol | Audience |
|---|---|---|---|
| Universal HTTP Adapter | Expose public REST APIs to external clients | HTTP REST | Browsers, mobile apps, third-party integrations |
| Command Transport | Execute commands locally or via remote microservices | HTTP RPC, gRPC | Internal service-to-service communication |
Both systems are built on the EDK's command pattern. Every operation, generating a key, resolving a DID, validating a credential, is a ServiceCommand with a typed input and output. The difference is how that command reaches the implementation that executes it.
Monolith vs Microservices
The EDK is designed so that the same application code can be deployed as a monolith or as distributed microservices without code changes. The decision is purely a matter of configuration and which modules are on the classpath.
Monolith Deployment
All command implementations run in the same process. Every command resolves to LOCAL execution:
Include all -impl modules in your classpath and every command executes locally. No network overhead, no serialization cost, no separate services to manage.
Microservice Deployment
Individual domains run as separate services. Commands that target a remote domain are transparently forwarded over HTTP RPC or gRPC:
The calling code is identical in both cases. A developer invoking kms.keys.generate writes the same code regardless of whether KMS runs in-process or on a remote node. The transport layer handles serialization, routing, and error mapping transparently.
How It Works
1. Command Invocation
You can invoke commands directly, but you don't have to. Service-layer classes like KmsService or DidService use commands internally, so whether you call a service method or a command, the transport routing applies equally:
// Option 1: Invoke the command directly
val result = generateKeyCommand.execute(
GenerateKeyInput(algorithm = "EC", curve = "P-256")
)
// Option 2: Use the service layer — internally delegates to the same command
val key = kmsService.generateKey(algorithm = "EC", curve = "P-256")
Both paths end up at the same ServiceCommand. If KMS is configured to run on a remote node, the command is forwarded transparently, regardless of whether you called the command or the service.
This means local vs remote execution is entirely invisible to the caller. You never need to know or care whether a particular domain is in-process or on another machine.
2. Routing Decision
The ExecutionTargetResolver determines where the command runs, based on hierarchical configuration:
| Target | Behavior |
|---|---|
LOCAL | Execute in-process via the session-scoped command registry |
SERVER | Forward to a remote service over HTTP RPC or gRPC |
AUTO | Try the configured remote transport; fall back to LOCAL if unavailable |
3. Transport Selection
When a command is routed to SERVER, the configured transport type determines the protocol:
| Transport | Path Convention | Use Case |
|---|---|---|
HTTP | POST /rpc/{module}/{service}/{action} | Cross-platform, firewall-friendly |
GRPC | {module}.{Service}Service/{Action} | High-throughput, streaming support |
Both transports carry the same payload, the command's serialized input, and return the same typed result. Switching between them is a one-line config change.
4. Transparent Execution
From the developer's perspective, there is no difference:
// This code works identically whether KMS is local or remote
val key = generateKeyCommand.execute(input)
If KMS is local, the command executes directly. If KMS is remote, the transport layer:
- Serializes the input (JSON, CBOR, or Protobuf)
- Sends it to the configured endpoint
- Deserializes the response
- Returns the same
IdkResult<T, IdkError>type
Errors from remote services are mapped to the same IdkError types used locally (e.g., NOT_FOUND_ERROR, UNAUTHORIZED_ERROR), so error handling code doesn't need to distinguish between local and remote failures.
Configuration
A single configuration block controls all routing decisions:
sphereon:
transport:
routing:
defaults:
target: LOCAL # Default: everything runs locally
transport: HTTP # Protocol when routing to SERVER
timeoutMs: 30000
modules:
kms: # Route all KMS commands to a remote service
target: SERVER
endpoint: "kms.internal:9090"
transport: GRPC
did:
target: SERVER
endpoint: "https://did-service.internal"
transport: HTTP
This configuration means:
- All commands default to
LOCALexecution kms.*commands are forwarded tokms.internal:9090via gRPCdid.*commands are forwarded tohttps://did-service.internalvia HTTP RPC- Everything else (party, credentials, etc.) runs in-process
See Command Transport for the full configuration reference and transport details.
Classpath-Driven Assembly
The transport behavior depends on which modules are on your classpath:
| Module | Provides |
|---|---|
lib-transport-routing-api | Core routing logic, local transport, configuration |
lib-transport-client-api | HTTP RPC client (Ktor-based) |
lib-transport-client-grpc | gRPC client (Netty-based, JVM only) |
lib-transport-codec-binary | CBOR and Protobuf serialization codecs |
Domain -impl modules | Local command implementations |
If a domain's -impl module is on the classpath, its commands can run locally. If the transport client modules are on the classpath and a domain is configured with target: SERVER, those commands are forwarded remotely.
A typical monolith includes all -impl modules. A typical gateway service includes transport clients and only the -impl modules for domains it handles locally.