Service-to-Service Integration
Service-to-service communication is one of the most common—and most complex—parts of modern systems.
Traditionally, this communication is implemented using:
- REST APIs
- gRPC services
- message-based RPC layers
Graftcode approaches this problem from a different angle.
Instead of designing endpoints, routes, or schemas, services expose public code, and other services consume it as a typed dependency.
Services as modules, not endpoints
In Graftcode, a service is treated like a module with a public interface.
What a service exposes is:
- a set of public classes
- with public methods
- defined using normal programming language constructs
What it does not expose:
- URLs
- HTTP verbs
- transport-specific payloads
- serialization concerns
From the consumer’s perspective, calling another service looks like calling a method on a local dependency.
Stateless service-to-service calls
The simplest pattern is stateless integration.
In this model:
- services expose static methods
- each call is independent
- no execution context is retained between calls
This closely matches how REST-style APIs are typically used, but without requiring:
- controllers
- DTO mapping layers
- HTTP semantics
Stateless calls are well suited for:
- business APIs
- query-style operations
- request–response workflows
They scale easily and work naturally behind load balancers.
Stateful service-to-service interactions
Some integrations require state.
Examples include:
- long-running workflows
- conversational processes
- subscriptions and event streams
- coordinated operations across multiple calls
Graftcode supports stateful service-to-service integration by allowing:
- object instances to live across calls
- callbacks and events
- duplex communication
Once a stateful interaction is established:
- execution context is preserved
- subsequent calls are routed consistently
- events can flow back to the caller
This replaces patterns that traditionally require WebSockets, SignalR, or custom session management.
Strong typing across service boundaries
All service-to-service calls are:
- strongly typed
- validated at compile time
- discoverable through IDE tooling
This eliminates a large class of integration errors:
- mismatched payloads
- missing fields
- undocumented behavior changes
If a service interface changes incompatibly, consumers find out immediately—before deployment.
Error handling between services
Errors are propagated using exceptions, not status codes.
If a service throws:
- a system exception
- or a custom business exception
the caller receives:
- a corresponding native exception
- with preserved error information
- and a connected trace context
Service-to-service error handling uses the same try/catch patterns as local code.
Evolving service contracts safely
Because service interfaces are versioned and consumed as dependencies:
- older and newer consumers can coexist
- upgrades are explicit
- compatibility is validated through builds and tests
This enables gradual evolution without synchronized deployments.
Configuration-driven architecture
Where a service runs is not a programming concern.
A service can:
- start as an in-memory module
- move to another runtime
- be deployed as a remote service
- be scaled independently
The integration code remains unchanged.
Routing decisions are handled through configuration, not application logic.
Why this pattern works
This pattern works because it:
- aligns with how developers already think about code
- removes transport concerns from business logic
- makes contracts explicit and enforceable
- improves tooling and developer feedback
Service-to-service communication becomes a matter of code reuse, not protocol negotiation.
In short
With Graftcode, service-to-service integration:
- uses normal programming interfaces
- is strongly typed and discoverable
- supports stateless and stateful workflows
- propagates errors and context naturally
- evolves safely over time
Services stop talking in protocols—and start talking in code.
See also: Edge Clients Without APIs