---
# System prepended metadata

title: 'Design: Simplifying FTL''s Backend Architecture'
tags: [draft]

---

# Design: Simplifying FTL's Backend Architecture
**Author**: @aat 

## Description (what)

Create a temporary "v2" implementation of our backend services that does not rely on any existing Controller implementation details, as per the architecture diagram that came out of the recent AU onsite:

<iframe src="https://link.excalidraw.com/readonly/lVYyIdgUs8O6MFf2lH2I" width="100%" height="400px" style="border: none;"></iframe>

All services will initially be in-memory only to simplify implementations, but with a goal of adopting [Raft](https://en.wikipedia.org/wiki/Raft_(algorithm)) in the near future where applicable. To that end, each service will decouple its state management from its service layer, so that when we do adopt Raft only the service layer needs to be modified.

## Motivation (why)

Recent experiences with our first customer have made it clear that evolving a complex stateful platform that live customers are building on top of is very difficult. Projecting schema state onto SQL tables made sense while the ideas behind FTL were being discovered, but over time this approach has slowed us to a crawl. At our recent AU onsite we realised that we can vastly simplify the FTL backend architecture by adopting a centralised "SchemaService" that is the single source of truth for the entire FTL cluster. All other services will consume from the schema and project the relevant information into in-memory data structures. The schema will contain not just the type information, but also all runtime information including deployments, configuration, secrets, etc.

From the diagram above, routing, ingress, console, controller, etc. will all consume the schema, updating their own internal state accordingly.

## Goals

- Simplify the FTL server architecture in order to increase velocity and decrease maintenance burden.

### Non-Goals

- No low-level implementation details, this is a proposal for how we can achieve our goals quickly.
- Don't support persistence, initially.

## Design (how)

Evolving the current controller to this new world would be very difficult. Instead, I propose that we start a new `v2/backend` folder with very simple, in-memory implementations of the new services. Once the system is functional, we will:

1. Delete the existing backends.
1. Adopt persistence for the SchemaService (possibly Raft), TimelineService (possibly Kafka), and evaluate other uses of persistence where appropriate.

Each service will be designed with its state management separate from its service layer, so that adopting persistence will be relatively straightforward when we get to that point.

Once the SchemaService is implemented, Parallelising this work will be very straightforward as the separate services are completely decoupled: routing, ingress, etc. simply pull from the SchemaService and update their internal state.

## Routing Service Pseudo-example

This is an example of what the RoutingService state management might look like. It is completely decoupled from the service layer because in a traditional RPC service, changes would be streamed directly from the SchemaService, but if we do end up adopting Raft, all changes must go through the Raft layer. This is analogous to a DAL.

```protobuf
service RoutingService {
  rpc Ping(PingRequest) returns (PingResponse);
  rpc Call(CallRequest) returns (CallResponse);
}
```

Example state management for the RoutingService (ignoring locking, etc.):

```go
type RouteState struct {
  routes *xsync.MapOf[string, *url.URL]
 

func NewRouteState(ctx context.Context, changes chan *schema.Module) *RouteState {
  svc := &SchemaServiceState{
    routes:  xsync.NewMapOf[string, *url.URL](),
  }
  go svc.updateRoutes(changes)
  return svc
}

// Get the endpoint for a verb.
func (s *RouteState) Get(ref schema.Ref) (*url.URL, bool) {
  return s.routes.Load(ref.String())
}

func (s *RouteState) updateRoutes(ctx context.Context, changes chan *schema.Module) {
  for {
    select {
    case <-ctx.Done():
      return
      
    case change := <-changes
      // Update routing table based on schema changes.
    }
  }
}

// ...
```


### Required changes (how)

1. Create new proto directory `backend/protos/xyz/block/ftl/v2alpha1`
2. Create new CLI tree `frontend/cli/v2` mirroring existing commands that will be replaced.
3. Implement SchemaService.
4. Implement new backends, CLI and BFF support.
5. Delete old Controller code.
6. Add persistence to SchemaService and other backends.


