Gateways — calling out

A gateway is egress: a declared HTTP call to something outside your application. You state the address, the shapes, the authentication, the timeout and the retry policy, and the client is generated. There is no hand-written code in a gateway at all.

That last point is the whole reason to choose one:

The frameworkYou own
gatewaywrites the clienta declaration
bespokewrites nothinga source file, forever

Reach for a bespoke when the framework cannot do it — entropy, a custom algorithm, an SDK with its own protocol. Reach for a gateway when it can. Most integrations are gateways, and most codebases discover this late.

sub-domains/notifications/gateways/mailer.vishwakarma
gateway MailerGateway {
  doc "Transactional email egress. One endpoint, discriminated by `template`."

  http {
    base_url_env "MAILER_URL"
    timeout      "10s"
    auth bearer_token: "MAILER_API_TOKEN"
  }

  retry {
    max_attempts  3
    backoff       exponential
    initial_delay "1s"
  }

  operation SendBookingConfirmation {
    request  SendMessageRequest
    response SendMessageResponse
    method   post
    path     "/v1/messages"

    discriminator "request.template == 'booking_confirmation'"
  }
}

Keep it pure egress

This gateway renders nothing and reads no entity. Every operation takes an already-rendered envelope.

That is deliberate. Rendering a message needs the booking, the provider's name, the customer's locale — application logic. A gateway that reached back into your domain to fetch them would be an integration with an opinion about your model, and it would break every time the model moved.

So the flow is always two steps: something assembles the message, and the next step hands it to the gateway.

Retry is a property of the integration

Declared once, not per call site. Exponential rather than fixed, because a provider that is rate-limiting you is made worse by a steady drumbeat.

⚠️ A retry policy is only safe where a repeat is. Sending the same email twice is tolerable and losing it is not, so retries are right here. An operation that charges somebody needs an idempotency key on the wire before it can carry one — a retried payment is a second payment.

When several operations share one path

Many providers expose a single endpoint discriminated by the payload, and that shape has a consequence worth understanding before you copy it: four typed operations that look identical on the wire are, to the counterparty, one operation. The distinction is yours alone and lives only in which method your code called.

A discriminator restores it — a predicate over the request that says which operation a given call is. It matters most the moment the gateway is stood in for, because a stand-in has to answer "which operation is this?" from the bytes, and it cannot invent the answer.

Note the discriminating field is a literal supplied per call site, not something the renderer produces: what it names is which operation this is, which the caller knows and the renderer does not.

The rest of the surface

BlockFor
httpbase URL (from an environment variable), timeout, authentication
retryattempts, backoff, initial delay
codecwhen the wire format is not plain JSON
cachewhen responses are worth holding
observespans and metrics for the calls
websocket / socketfor protocols that are not request/response
virtualthe generated stand-in — see Service virtualization

Why the base URL is an environment variable

base_url_env "MAILER_URL" names a variable rather than a URL, so the same build talks to a real provider in production and to a local mail catcher on a laptop with nothing in the model changing.

The credential works the same way and is never a literal. A gateway declaration is a description of an integration, and a description with a secret in it is a secret in your source control.