News

Services Arrive in Ritchie

Ritchie services give orchestration a first-class home, with checked boundaries, typed outcomes and no deployment addresses in application code.

Tahir Hashmi · 09 October 2026

Ritchie programs can now declare stateless services and call them on the local monolith target. Service methods cross a checked value boundary and return a typed result, a declared domain error, Unavailable! or Rejected!.

Services give orchestration a first-class home. An ordinary function call is local implementation detail; a service call names a logical destination in the program. Source code says @Pricing.quote(...), without embedding a host, port or deployment address.

pricing.riRitchie
UnknownSku! { String sku }

@Pricing { }

@Pricing.quote(String sku) -> Int | UnknownSku! {
  if sku != "TEA" {
    return UnknownSku! { sku }
  }
  return 350
}

main() {
  Int cents = @Pricing.quote("TEA") handle {
    UnknownSku!  => 0
    Unavailable! => 0
    Rejected!    => 0
  }
  log(cents)
}

Stateless by Declaration

A service declaration has an empty body. It owns no fields and there is no service instance to retain data between calls. Its methods live at module level on the @ type and compute from their arguments, module code, entity operations and calls to other services.

This restriction keeps two responsibilities separate. Entities own domain state and protect their invariants. Services coordinate work across entities and other services. A service method may sequence several operations, but that sequence is not presented as one transaction.

This is a useful constraint. A long-lived service object with mutable fields would make a method's result depend on state that is absent from its signature and tied to one runtime instance. Stateless methods make their inputs visible and let the logical service remain independent of its eventual placement.

Compilation Checks at the Service Boundary

Every service parameter and result must be a public value with value semantics. The compiler checks nested fields, collections, dictionaries, aliases and nullable values too. Function values, entity references and other values that cannot cross the boundary are rejected at the method declaration—even if the method is not called. Hopefully that settles the throwbacks to CORBA *wink*.

RITE, the runtime model under Ritchie, keeps failures of the mechanism apart from failures of the application. A runtime failure means an operation did not run, or its outcome is unknown, because of something like a timeout or a lost connection. RITE recognises four of them:

  • Unavailable!: recovery ended without a result, and the relevant state is confirmed unchanged.
  • Rejected!: the runtime refused the invocation for a code or configuration defect, before the relevant state changed.
  • Unverified!: an operation that can change state may have done so, and its final effect is not established.
  • Conflict!: an entity invocation ran out of attempts under contention, and its state is unchanged.

The runtime handles these first, by retrying or failing over, without any application code. A program sees one only after that recovery is exhausted, and can then match it in handle like any other error. Application code cannot construct or raise these errors, and they never appear in a method signature.

Domain failures (that's our term for regular application errors) are part of the method signature. If a service method could let an undeclared domain error escape, the compiler flags it as an error, instead of letting it become a post-deployment timebomb in production.

A domain error such as UnknownSku! can take a domain-specific recovery path, while an unavailable destination or a deployment misconfiguration can be reported and handled differently.

Logical Names, Physical Placement Later

The @Pricing name identifies the destination that the program intends to call. It does not say whether the implementation shares a process, runs on another machine or has several instances. Distribution topology belongs to the runtime target, not to each call site.

Currently, the only supported target is a monolith. Calls still cross the service boundary and its value codec, but execution stays within one process. That gives the language a real service model without asking application code to commit to a network layout. Soon, though the same program would be deployable as independent processes on a single server as well as on a cluster in the form of independent pods. No more monoliths vs. microservices debates required!

A Complete Local Execution Path

A resident application compiles to a one-process .run bundle. ritchie run starts the bundle, and --serve makes every service method available on a loopback HTTP address.

The loopback interface is useful for running and inspecting a resident application on one machine. It is not multi-process or cluster deployment, and it is not the public HTTP gateway. This release establishes the local service path: declaration, checking, calling, result transport, hosting and admission failure.

Services are now a language concept rather than a convention assembled from functions and infrastructure. The program comprises logical units and their contracts. The local target can execute that model today without putting deployment coordinates into application code.

Try building services in the Ritchie playground now!