Skip to content

Core Concepts

Oleksandr Geronime edited this page Jun 29, 2026 · 3 revisions

Core Concepts

This page covers the mental models that underpin SERP. Understanding these will make the generated code, the workspace layout, the deployment model, and the AI session model straightforward rather than mysterious.

SERP is built around a simple engineering constraint: AI can be useful in large C++ systems only when the work is bounded by explicit contracts. The framework turns architecture into specs, specs into generated infrastructure, and services into isolated implementation scopes.

The historical SERP name still describes the system:

Pillar Core idea
Service A bounded implementation unit with a typed interface and isolated event loop.
Event The observable behavior surface: calls, properties, notifications, streams, timers, and watchdogs.
Runtime Development-time inspection and control of running services.
Platform Generation, transports, deployment, lifecycle, logging, testing, IDE tooling, and AI session enforcement.

Spec-First Development

To automate anything reliably — especially with AI — you need to describe it precisely first. In SERP, that description is the spec, and everything else follows from it.

SERP starts with contracts, not implementations.

You write service interfaces in SerpIDL — a typed interface definition language. The code generator reads those specs and produces a complete C++ skeleton. You write business logic into the stubs it creates.

specs/serp.sidl   →   serpgen   →   gen/   →   src/ (your code)
    (contract)      (generator)   (skeleton)   (implementation)

The spec is the single source of truth for method signatures, argument types, events, and properties. If you want to change an interface, you change the spec and regenerate — you never edit the generated files directly.

Practical rule: gen/ is disposable. src/ is yours.

For AI-driven development, this rule is more than convenience. It defines the boundary between deterministic infrastructure and implementation judgment. AI sessions operate where judgment is needed; generated code covers the repeatable wiring.


Service-Oriented Architecture

Isolated event loops enforce a hard constraint: a service cannot accidentally share state with another. This boundary is what makes services independently implementable — and what makes parallel AI sessions safe by construction.

Each SERP service:

  • Runs on its own named event loop (single-threaded)
  • Exposes a typed interface to other services
  • Receives calls through a generated dispatch table
  • Has no shared mutable state with other services

Cross-service calls are typed method invocations on interface pointers. There are no global variables, no shared queues, and no direct thread handoffs between services. The framework enforces this boundary at the generated code level.

┌───────────────────────────┐     ┌───────────────────────────┐
│  Service A (EventLoop A)  │     │  Service B (EventLoop B)  │
│                           │     │                           │
│  IBar* bar_;              │────►│  BarBase (dispatch)       │
│                           │     │          │                │
│  bar_->doSomething(x, cb) │     │          ▼                │
│                           │     │  BarImpl::doSomething     │
│                           │     │  (your code)              │
└───────────────────────────┘     └───────────────────────────┘

The event loop guarantees that a service's methods execute one at a time. You do not need locks inside a service implementation.


Transport Abstraction

Deployment topology is an architectural decision, not a code decision. Transport abstraction enforces this separation — service code never knows whether its neighbor is in the same process or on a remote machine over DBus or gRPC.

The same service code works with multiple transport backends:

Backend Description Use case
inprocess Direct function calls, no IPC Monolith builds, unit tests
DBus Linux D-Bus IPC Multiprocess on Linux
POSIX Unix Domain Socket IPC Portable multiprocess with no external dependencies
gRPC gRPC over network sockets Portable multiprocess, cross-machine

The transport is selected in the deployment spec, not in the service code. A service never calls DBus::send(...) or gRpc::call(...). It calls bar_->doSomething(x, cb) — the generated adapter translates that into the right wire protocol.

This means you can develop and test entirely in a monolith (fast, debuggable, no IPC setup) and then deploy the same binary set over DBus or gRPC without touching a single line of service code.


Code Generation Guarantees

When something can be derived from the spec, it should be generated — not written by humans or AI. The generator takes responsibility for the entire deterministic layer: transport adapters, dispatch tables, process entry points, CMake wiring. What remains in src/ is business logic: the only part that genuinely requires implementation judgment.

Running serpgen generate-workspace follows these rules without exception:

  • Everything under gen/ is always overwritten.
  • Everything under src/ is never touched.

The generated inheritance chain for each service is:

IFoo                   (gen/interfaces/IFoo.h)
  └── FooBase          (gen/base/FooBase.h/.cpp)
        └── Foo        (gen/services/Foo.h/.cpp — generated once, then left alone*)
              └── FooImpl   (src/components/foo/FooImpl.h/.cpp — yours)
Class Location Who writes it Regenerated?
IFoo gen/interfaces/ serpgen Yes, every time
FooBase gen/base/ serpgen Yes, every time
Foo gen/services/ serpgen Yes, every time
FooImpl src/components/foo/ You Never

IFoo is a pure virtual interface. FooBase provides the wiring: it connects the transport adapter to the virtual methods, handles serialization, and manages the event loop integration. Foo is a thin default implementation (may be a no-op stub or a generated starting point). FooImpl is where you write business logic — it inherits from Foo and overrides only the methods you care about.


The Runtime is Engineering-Only

SERP includes a debug registry (Serp::runtime) that lets you call any registered service method by name at runtime, inspect properties, and inject test signals. This is exposed through the VS Code plugin and through serpgen tooling.

The runtime registry is disabled in release builds.

It exists to make development and debugging fast — you can poke a running service without writing a dedicated client. It must never be used in production code paths. If your production code calls Serp::runtime::invoke(...), that is a bug.


Deployment is a Separate Concern

The same service codebase should produce a monolith for development, a DBus-distributed system for Linux production, and a gRPC-distributed system for macOS — without touching a single line of service code. Deployment is a spec-level decision, not a code-level one.

A SERP workspace produces multiple build targets from the same service codebase:

  • Monolith: all services in one process, inprocess transport
  • Per-process binaries: one process per service (or per service group), DBus or gRPC transport

Which services run in which processes, and which transport connects them, is specified in the deployment section of serp.sidl. The service implementation code is identical across all deployment targets.

specs/serp.sidl
  ├── service Calculator { ... }     ← interface and behavior
  └── deployment monolith {          ← where and how it runs
        process main {
          includes: [Calculator, Console]
          transport: inprocess
        }
      }

To add a DBus-distributed deployment, you add a second deployment block — no changes to CalculatorImpl.cpp.


Summary: How the Pieces Fit Together

┌──────────────────────────────────────────────────────────────────┐
│  SerpIDL spec   →  serpgen  →  gen/interfaces/IFoo.h             │
│                                                                  │
│   IFoo (contract)                                                │
│     implemented by FooBase (generated wiring)                    │
│       extended by FooImpl (your business logic)                  │
│         consumed by BarImpl via IFoo* pointer                    │
│           wired together in gen/mains/main_monolith.cpp          │
│             transport connects services across processes          │
└──────────────────────────────────────────────────────────────────┘

The spec defines the contract. The generator produces the skeleton. You write the logic. The runtime connects everything. The deployment spec controls the topology.

Clone this wiki locally