Proof of concept
The same discipline, in any language
One billing system, four languages. On the left, idiomatic source. On the right, its Trellis sidecar — the same structured statement of intent, whatever the code is written in.
A .trellis sidecar states a unit’s contract — what it provides, what it
consumes, what must always be true, and what is explicitly not its
job — in one uniform format that does not change from language to language.
The handles adapt to each ecosystem’s idioms (Go package paths, TypeScript
Type: and Event: prefixes, SQL Table: and View: objects); the structure,
and the discipline it imposes, stay identical.
That uniformity is the point. The sidecar is highlighted below by the same grammar that ships in the Trellis editor extension — not faked for the page.
# frozen_string_literal: true
# Creates a user subscription and performs the initial charge.
# Idempotent on (user, plan_id, idempotency_key).
class CreateSubscription
class PaymentError < StandardError; end
def initialize(gateway: PaymentGateway.new)
@gateway = gateway
end
# @return [Subscription]
# @raise [PaymentError] when the gateway declines the charge
def call(user:, plan_id:, idempotency_key:)
plan = Plan.find(plan_id)
charge = @gateway.charge(
token: user.payment_token,
amount: plan.price_cents,
idempotency_key: idempotency_key,
)
raise PaymentError, charge.error unless charge.ok?
subscription = Subscription.create!(user: user, plan: plan, status: :active)
Events.publish("subscription.created", subscription_id: subscription.id)
subscription
end
end
@owner: BillingTeam
@stability: stable
@composition: [Authenticatable, Payable]
@since: 2025-11-04
@reviewed: 2026-03-15
Feature: Create Subscription
"Handles the idempotent creation of a user subscription and initial billing."
Provides:
- Subscription.create(user, plan_id) -> Subscription | raises PaymentError
- Event: subscription.created
Consumes:
- PaymentGateway.charge(token, amount) -> ChargeResult
- UserRecord (must respond to: id, email, payment_token)
Invariants:
- A user MUST NOT have two active 'pro' subscriptions
- Charges SHALL be idempotent on (user_id, plan_id, idempotency_key)
OutOfScope:
- Refunds (handled by RefundService)
- Plan upgrades (handled by ChangeSubscriptionPlan)
Scenario (happy-path): Successful checkout
Given a User with a valid stripe_token
When the create method is called with plan_id
Then a Subscription record is created
And the PaymentGateway receives a charge request
And the User status becomes active
Scenario (negative): Expired card
Given a User with an expired card
When the create method is called
Then it MUST raise PaymentError
And no Subscription record is created
left the code ·
right its .trellis sidecar — the intent both you and an agent read before touching the code.
// Package billing computes prorated charges for mid-cycle plan changes.
package billing
import (
"errors"
"time"
)
// ErrInvalidPeriod is returned when a period's End precedes its Start.
var ErrInvalidPeriod = errors.New("billing: period end precedes start")
// Money is a count of minor units (e.g. cents) in a single currency.
type Money int64
// Period is the billing window a proration is computed against.
type Period struct {
Start time.Time
End time.Time
}
// Proration computes the amount owed when a subscription changes plan
// partway through a billing period. Time enters through now so the unit
// stays pure and testable.
type Proration struct {
now func() time.Time
}
// Calculate returns the amount owed for the remainder of period when
// moving from oldPlan to newPlan. A negative result is a credit.
func (p Proration) Calculate(period Period, oldPlan, newPlan Money) (Money, error) {
if period.End.Before(period.Start) {
return 0, ErrInvalidPeriod
}
elapsed := p.now().Sub(period.Start).Seconds()
total := period.End.Sub(period.Start).Seconds()
remaining := 1 - elapsed/total
delta := float64(newPlan-oldPlan) * remaining
return Money(roundHalfUp(delta)), nil
}
@owner: BillingTeam
@stability: stable
@layer: domain
@since: 2025-11-04
@reviewed: 2026-03-15
Feature: Proration
"Computes the credit or charge owed when a subscription changes plan mid-period."
Provides:
- billing.Proration.Calculate @source("symbol:Calculate") -> Money | error
Consumes:
- billing.Period (must carry: Start, End)
- billing.roundHalfUp @source("symbol:roundHalfUp") -> int64
Invariants:
- The result MUST be denominated in the period's currency
- A downgrade with time remaining MUST yield a credit (negative Money)
- Calculate MUST NOT read the wall clock directly; time enters through the injected now()
OutOfScope:
- Applying the proration to an invoice (handled by billing.Invoice.Apply)
- Currency conversion (handled by fx.Convert)
Scenario (happy-path): Upgrade halfway through the period
Given a monthly period that is 50% elapsed
When Calculate is called with a higher newPlan
Then it returns a positive charge for the remaining half
Scenario (negative): Inverted period bounds
Given a Period whose End precedes its Start
When Calculate is called
Then it MUST return ErrInvalidPeriod
left the code ·
right its .trellis sidecar — the intent both you and an agent read before touching the code.
import type { ApiClient } from "../platform/api-client";
import { publish } from "../platform/events";
export interface InvoiceDTO {
id: string;
customerId: string;
amountDue: number; // minor units
currency: string;
status: "open" | "paid" | "void";
}
/** Creates open invoices for customers and announces them. */
export class InvoiceService {
constructor(private readonly api: ApiClient) {}
async createInvoice(
customerId: string,
amountDue: number,
currency: string,
): Promise<InvoiceDTO> {
if (amountDue <= 0) {
throw new RangeError("amountDue must be positive minor units");
}
const invoice = await this.api.post<InvoiceDTO>("/invoices", {
customerId,
amountDue,
currency,
status: "open",
});
publish("invoice.created", { invoiceId: invoice.id, customerId });
return invoice;
}
}
@owner: BillingTeam
@stability: stable
@layer: application
@since: 2025-11-04
@reviewed: 2026-03-15
Feature: Invoice Service
"Creates open invoices for customers and announces them to the rest of the system."
Provides:
- billing.InvoiceService.createInvoice @source("symbol:createInvoice") -> InvoiceDTO
- Type: Billing.InvoiceDTO @source("symbol:InvoiceDTO")
- Event: invoice.created
Consumes:
- ApiClient: Platform.post -> InvoiceDTO
- billing.events.publish @source("symbol:publish")
Invariants:
- amountDue MUST be positive minor units
- Every created invoice MUST start in the 'open' status
- invoice.created MUST be published only after the invoice persists
OutOfScope:
- Charging the customer (handled by billing.PaymentService.charge)
- Dunning and retries (handled by billing.DunningJob)
Scenario (happy-path): Create an open invoice
Given a customer and a positive amountDue
When createInvoice is called
Then it persists an 'open' invoice
And it publishes invoice.created with the new invoice id
Scenario (negative): Non-positive amount
Given an amountDue of 0
When createInvoice is called
Then it MUST raise a RangeError
And it MUST NOT publish invoice.created
left the code ·
right its .trellis sidecar — the intent both you and an agent read before touching the code.
-- analytics.monthly_revenue
-- Recognized revenue per calendar month, in each invoice's settlement currency.
CREATE OR REPLACE VIEW analytics.monthly_revenue AS
SELECT
date_trunc('month', p.captured_at AT TIME ZONE 'UTC') AS month,
i.currency,
SUM(p.amount_minor) AS revenue_minor
FROM billing.payments AS p
JOIN billing.invoices AS i
ON i.id = p.invoice_id
WHERE p.status = 'captured'
AND i.status = 'paid'
GROUP BY 1, 2
ORDER BY 1, 2;
@owner: AnalyticsTeam
@stability: stable
@layer: reporting
@since: 2025-11-04
@reviewed: 2026-03-15
Feature: Monthly Revenue
"Recognized revenue per calendar month, in each invoice's settlement currency."
Provides:
- View: analytics.monthly_revenue @source("symbol:monthly_revenue")
Consumes:
- Table: billing.payments (must carry: amount_minor, status, captured_at, invoice_id)
- Table: billing.invoices (must carry: id, currency, status)
Invariants:
- Revenue MUST count only 'captured' payments against 'paid' invoices
- Months MUST be bucketed in UTC
- Amounts MUST stay in minor units; the view MUST NOT convert currencies
OutOfScope:
- Refund netting (handled by View: analytics.net_revenue)
- Conversion to a single reporting currency (handled by View: analytics.revenue_usd)
Scenario (happy-path): A captured payment on a paid invoice
Given a captured payment of 1000 minor units on a paid USD invoice in March
When monthly_revenue is queried
Then March / USD revenue includes those 1000 minor units
Scenario (edge): A pending payment
Given a payment with status 'pending'
When monthly_revenue is queried
Then that payment MUST NOT contribute to any month's revenue
left the code ·
right its .trellis sidecar — the intent both you and an agent read before touching the code.