Foundations
Domain × Context → Resolution
The equation is not decoration. It is a discipline you can apply on any project, in any language: name what is yours, name what is the platform's, and refuse to let the second leak into the first.
#Read the equation
The principle
Domain is the multiplicand you own: entities, rules, use cases. Context is the multiplier you inherit: runtime, transport, input and output shapes. Resolution is the product: a concrete response, here and now.
It is a product, not a sum, because the domain is expressed through the context, once per event. Change the context and you get a different resolution of the same domain, not a different application.
In Stone.js
In Stone.js the domain is your handlers and services. The context is an IncomingEvent produced by an adapter plus an ephemeral container. The resolution is what the kernel returns after applying one to the other.
You never construct the context. You receive it. That single constraint is what keeps the domain portable.
#Keeping the domain pure
Here is the rule that makes the equation real: a handler receives an intention, never a platform. It reads values from an event; it does not know if that event came from an HTTP request, a CLI invocation, a browser navigation or an agent call.
import { RuntimeError } from '@stone-js/core'
import { IncomingHttpEvent } from '@stone-js/http-core'
import { Get, Post, Delete, EventHandler } from '@stone-js/router'
// Pure domain. Every value is an intention read off the event; nothing here
// knows whether it arrived over HTTP, a CLI flag, or an agent tool call.
@EventHandler('/tasks')
export class TaskController {
private readonly tasks: TaskService
constructor ({ tasks }: { tasks: TaskService }) { this.tasks = tasks }
@Get('/')
list (event: IncomingHttpEvent): Task[] {
return this.tasks.list({
done: event.get<boolean>('done'),
limit: event.get<number>('limit', 50)
})
}
@Get('/:id')
show (event: IncomingHttpEvent): Task {
const task = this.tasks.find(event.get<string>('id'))
if (task === undefined) throw new RuntimeError('Task not found')
return task
}
@Post('/')
create (event: IncomingHttpEvent): Task {
return this.tasks.add(event.get<string>('title'))
}
@Delete('/:id')
remove (event: IncomingHttpEvent): void {
this.tasks.remove(event.get<string>('id'))
}
}import { RuntimeError } from '@stone-js/core'
import { defineEventHandler, defineRoutes } from '@stone-js/router'
// Pure domain, as functions. Same intentions, same ignorance of the platform.
const TaskController = ({ tasks }) => ({
list: (event) => tasks.list({
done: event.get('done'),
limit: event.get('limit', 50)
}),
show: (event) => {
const task = tasks.find(event.get('id'))
if (task === undefined) throw new RuntimeError('Task not found')
return task
},
create: (event) => tasks.add(event.get('title')),
remove: (event) => tasks.remove(event.get('id'))
})
export const routes = defineRoutes([
[defineEventHandler(TaskController, 'list'), { path: '/tasks', method: 'GET' }],
[defineEventHandler(TaskController, 'show'), { path: '/tasks/:id', method: 'GET' }],
[defineEventHandler(TaskController, 'create'), { path: '/tasks', method: 'POST' }],
[defineEventHandler(TaskController, 'remove'), { path: '/tasks/:id', method: 'DELETE' }]
])Nothing in that code names a server. Give it a Node adapter and it is a REST API; give it an MCP adapter and the same two methods become tools an agent can call. The domain did not change; the context did.