Partitions and resizing
A named partition is a separate database (its own bundle, data, timers and retry receipts) that you can move between Raft groups. Several partitions share one group.
- Choose partitions when you create a database. Data in the default database can't be turned into a partition later.
- A partition can't read another partition's data directly. A transaction can call exposed methods in several partitions at once.
import { FlowerAdmin, FlowerClient } from "@flower-js/sdk/client";
import { buildBundle } from "@flower-js/sdk/bundle";
const admin = new FlowerAdmin("http://catalog-1:7101", { adminToken: process.env.FLOWER_ADMIN_TOKEN });
await admin.registerGroup({ id: "west", addresses: ["west-1:7101", "west-2:7101", "west-3:7101"] });
await admin.registerGroup({ id: "east", addresses: ["east-1:7101", "east-2:7101", "east-3:7101"] });
await admin.createPartition("tenant-a", "west", { requestId: "create-tenant-a" });
await admin.waitForPartition("tenant-a");
await admin.partition("tenant-a").deploy(await buildBundle("app.ts"), { requestId: "deploy-tenant-a" });
const tenant = new FlowerClient("http://catalog-1:7101").partition("tenant-a"); // Calls its exposed methods.
await admin.resize(["west", "east"], { requestId: "grow-two-groups" });
console.log(await admin.layout()); // Durable progress; resize continues after this client exits.| API | Contract |
|---|---|
client.partition(name): FlowerClient<App> | A client for /partitions/{name}, keeping the same transport, credentials, retry policy and types. Works wherever the partition lives. Isolation only; it doesn't authenticate tenants. |
admin.partition(name): FlowerAdmin | Operator calls against one named database: deploys, staged deployment, keys, retention and transaction closure. |
admin.layout(options?): Promise<ClusterLayout> | Current groups, placements, moves and rebalance plan. |
admin.registerGroup(group, options?): Promise<ClusterGroup> | Register a group that is already running. Safe to repeat. Doesn't start servers. |
admin.removeGroup(id, options?): Promise<{removed: string}> | Unregister an empty, unused group (never the catalog). Doesn't stop servers or delete files. |
admin.createPartition(name, group, options?): Promise<PartitionPlacement> | Create an empty partition. Wait until active, then deploy to it. |
admin.movePartition(name, destination, options?): Promise<PartitionMove> | Start moving to another group. Returns before the move finishes; the move continues if the client goes away. |
admin.resize(groupIds, options?): Promise<RebalancePlan> | Spread partitions evenly by count across these groups, one move at a time; groups left out are drained. Doesn't balance by size or load. Register new groups first; remove drained ones after. Rejected while another plan or move is running. |
admin.partitionStatus(name, options?): Promise<PartitionPlacement> | Current phase, owner and epoch. |
admin.waitForPartition(name, options?): Promise<PartitionPlacement> | Poll until active. Defaults: timeoutMs 30,000, intervalMs 100. Throws PARTITION_WAIT_TIMEOUT on timeout; abort only stops waiting. |
ControlOptions | Optional signal and requestId. A UUID is generated if omitted; set your own so you can retry safely. |
PartitionWaitOptions | Optional signal, timeoutMs and intervalMs. |
ClusterGroup | {id: string, addresses: string[]}, with distinct host:port entries. Addresses are seeds and can't be changed while the group is registered; keep at least one reachable. |
PartitionPlacement | {partition, epoch, owner: ClusterGroup, status: "creating" | "active" | "moving", operation, movement: PartitionMove | null}. |
PartitionMove | {operation, partition, source: ClusterGroup, destination: ClusterGroup, source_epoch, epoch, phase}. The epoch changes on each move; revision and receipts carry over. |
PartitionMovePhase | "copying" | "freezing" | "importing" | "activating" | "retiring" | "complete". Moves always finish; there is no cancel. |
RebalanceMove | {partition, source, destination, operation}, with group IDs. |
RebalancePlan | {operation, groups: ClusterGroup[], moves: RebalanceMove[], next: number, complete: boolean}. next is the index of the next move. |
ClusterLayout | {groups: ClusterGroup[], partitions: PartitionPlacement[], moves: PartitionMove[], rebalance: RebalancePlan | null}. Move history is kept indefinitely. |
Running partitions
Configure every server with:
FLOWER_GROUP: this server's group.FLOWER_CATALOG_GROUP: the group that stores placements.FLOWER_GROUPS: JSON map of group IDs tohost:portlists, including this group and the catalog.- The same peer secret everywhere, and a separate operator token.
Initialize each group on its own. Put applications in named partitions, not the catalog's default database.
During a move
- The source keeps serving while it copies, then briefly freezes to send the final changes. Writes either land before the freeze or fail; none are lost.
- The freeze can take longer for large partitions or slow networks. It is not guaranteed to be milliseconds.
- If a group loses quorum after the freeze, the move pauses until it recovers.
- Pending cross-group transactions delay the freeze.
- Live watches end with an error when the owner changes. Reconnect through the partition client; there's no automatic replay.
- Other partitions on the same group keep working but share CPU, disk and the Raft log.
| Setting | Effect |
|---|---|
FLOWER_ROUTE_CACHE_MS | How long a server trusts a cached owner. Default 1,000 ms. If the catalog is down when an entry expires, requests to that partition fail. |
FLOWER_PARTITION_TAIL_MAX_BYTES | If the final change set is bigger than this, send a full copy instead. Unset by default. |
FLOWER_PARTITION_BASE_MAX_BYTES | Maximum size of the initial copy. Unset by default. |
Partition URLs are not a security boundary. For public traffic, put a trusted gateway in front that limits each caller to its partitions. Retry uncertain mutations with the same request ID and body.
Cross-group transactions
A transaction method calls exposed methods in several partitions or groups and commits them all or none. The methods guide has a complete example.
| API | Contract |
|---|---|
transaction(name, plan)transaction(name, spec: MethodSpec, plan) | Returns a TransactionMethod<A, V>. plan(args) returns a TransactionPlan; it sees only its arguments and can’t read data. spec.args checks the arguments and spec.access guards the call. Expose it as an alias and call it with client.call. |
participant<Methods>(target).call(alias, args?): TransactionCall | Builds one call. Methods, an http map or typeof app, types the alias and its arguments; transactions can’t be participants. target is { partition } or { group }. |
TransactionTarget | Exactly one of partition: string (a named database) or group: string (a group’s root database). |
TransactionCall | A TransactionTarget plus method (an exposed query or mutation alias) and optional args. |
TransactionPlan<V = Json> | calls: readonly TransactionCall[] and optional value: V. The call list is fixed up front; results can’t choose later calls. |
TransactionResult<V = Json> | What the caller gets: results, one per call in plan order, and the plan’s value, when it returned one. |
TransactionMethod<A = Json, V = Json> | {kind: "transactionMethod", name, compute}. Create it with transaction; don’t call compute yourself. |
- Check business rules inside the participant methods. Later calls to the same target see earlier ones.
- If a participant fails, nothing commits and the caller gets
422 TRANSACTION_ABORTEDwith that participant’sfailure. It is final for that request ID; fix the problem and use a new ID. - Participants are authorized with the coordinator’s principal; see delegation.
- While a transaction is prepared, each participating database blocks fresh reads, watches, writes, deploys and maintenance, even for unrelated keys. Other partitions on the same group are unaffected.
- Replica-local reads may not see the transaction atomically.
- If the coordinator is unreachable, participants stay blocked until it recovers. Nothing times out on its own.
TRANSACTION_PREPARED: wait for recovery and retry with the same ID and content.- Completed transaction records are cleaned up only by operator action; see retry retention and transaction closure.
- Every participant needs
FLOWER_GROUP,FLOWER_GROUPSand the shared peer secret; named partitions also needFLOWER_CATALOG_GROUP.
Full protocol: transactions and recovery.