Scale by moving named partitions
To increase the number of Raft groups, start and initialize another group, register it, then call admin.resize() with the complete desired set of groups. To decrease it, resize onto the groups you want to keep, wait for completion, unregister the drained groups, and stop their servers.
Each Flower process belongs to one physical Raft group. A group has its own log, leader and quorum; several named partitions can share it. Adding replicas to a group changes its replication and fault tolerance. Adding groups gives partitions independent Raft logs and writers. The membership API handles replica changes within a group.
This walkthrough grows from one three-replica group, west, to two, west and east: three server processes become six. west also stores the partition catalog. A dedicated catalog group is supported too; count it separately if you leave it out of the partition-placement target set.
| Group | Role | Example replica addresses |
|---|---|---|
west | Existing group; catalog and named partitions | 127.0.0.1:7101, :7102, :7103 |
east | New group for named partitions | 127.0.0.1:7201, :7202, :7203 |
Partition count sets the granularity: two or more partitions can use two groups, but one large partition moves as a whole. Flower does not split its keys automatically. Data in a group's default database, including composite tenant keys, is not moved by a resize or automatically converted into named partitions.
Enable partitions on the existing group
Configure every west replica with these settings, alongside its existing operator and peer tokens:
export FLOWER_GROUP=west
export FLOWER_CATALOG_GROUP=west
export FLOWER_GROUPS='{"west":["127.0.0.1:7101","127.0.0.1:7102","127.0.0.1:7103"]}'For a new deployment, start and initialize the three replicas with that environment. For an existing initialized group, apply the settings on restart, keeping each node's ID, address and data directory; do not initialize it again. Keep the same designated catalog group throughout the resize.
The TypeScript snippets below form an operator script using the SDK. Connect it to a reachable west replica and register the existing group:
import { FlowerAdmin } from "@flower-js/sdk/client";
const admin = new FlowerAdmin("http://127.0.0.1:7101", {
adminToken: process.env.FLOWER_ADMIN_TOKEN,
});
await admin.registerGroup({
id: "west",
addresses: ["127.0.0.1:7101", "127.0.0.1:7102", "127.0.0.1:7103"],
});Registration is safe to repeat with the exact same group descriptor. Registered seed addresses are immutable until the group is removed. If you already registered west, use its existing descriptor from admin.layout().
Put the workload in named partitions before resizing: create a partition, wait for it to become active, and deploy its application. A new, empty cluster has no partitions to rebalance.
Start an additional Raft group
Use the same Flower build and shared peer token as west. Each new replica gets its own address and fresh data directory. The loopback addresses below let you try this on one machine; across hosts, use addresses peers can reach and the deployment's TLS configuration.
export FLOWER_ADMIN_TOKEN='same-operator-token-as-west'
export FLOWER_PEER_TOKEN='same-peer-token-as-west'
export FLOWER_GROUP=east
export FLOWER_CATALOG_GROUP=west
export FLOWER_GROUPS='{"west":["127.0.0.1:7101","127.0.0.1:7102","127.0.0.1:7103"],"east":["127.0.0.1:7201","127.0.0.1:7202","127.0.0.1:7203"]}'
# Run one command per terminal, with the environment above in each:
./target/release/flower --id 1 --listen 127.0.0.1:7201 --data .flower/east/node1
./target/release/flower --id 2 --listen 127.0.0.1:7202 --data .flower/east/node2
./target/release/flower --id 3 --listen 127.0.0.1:7203 --data .flower/east/node3Bootstrap east once, using only its own members. The explicit URL selects the new group:
export FLOWER_ADMIN_TOKEN='same-operator-token-as-west'
node sdk/cli.ts init --url http://127.0.0.1:7201 \
--members 1=127.0.0.1:7201,2=127.0.0.1:7202,3=127.0.0.1:7203
curl -fsS -H "Authorization: Bearer $FLOWER_ADMIN_TOKEN" \
http://127.0.0.1:7201/raft/metricsWait for the new group to elect a leader and establish quorum before registering it. FLOWER_GROUPS supplies bootstrap contacts; adding an entry there does not provision or register a group. Existing partition-enabled replicas learn new partition owners and their addresses from the catalog, so they can route to east without restarting to add it to their bootstrap map. Transactions that directly target { group: "east" } use the startup registry instead; configure those callers with the new group's peers.
Grow from one group to two
With east initialized and reachable, continue the operator script:
await admin.registerGroup({
id: "east",
addresses: ["127.0.0.1:7201", "127.0.0.1:7202", "127.0.0.1:7203"],
});
const growth = await admin.resize(["west", "east"], {
requestId: "grow-west-east-001",
});
console.log(growth);The argument is the complete, nonempty set of groups that should own named partitions, with no duplicates. To grow again, register another initialized group and include it alongside every group you want to retain. Groups omitted from the list are drained. Registration alone leaves existing placements where they are.
The returned plan records the moves durably; the call returns before they finish. Flower balances all named partitions by count, retains existing owners where possible, and moves one partition at a time. Use a new request ID for each intended resize, and reuse that ID and target set when retrying an uncertain request.
Wait for the resize to finish
Poll admin.layout() and check the operation ID and rebalance.complete. Add this helper to the same script:
import { setTimeout as delay } from "node:timers/promises";
async function waitForResize(operation: string) {
const signal = AbortSignal.timeout(15 * 60_000);
while (true) {
const layout = await admin.layout({ signal });
const plan = layout.rebalance;
if (plan?.operation !== operation) {
throw new Error("The catalog has a different resize plan; inspect admin.layout()");
}
console.log(`${plan.next}/${plan.moves.length} moves complete`);
if (plan.complete) return layout;
await delay(1000, undefined, { signal });
}
}
const layout = await waitForResize(growth.operation);
console.table(layout.partitions.map((p) => ({
partition: p.partition, group: p.owner.id, status: p.status,
})));The 15-minute deadline only stops this observer. A timeout, disconnected operator or catalog leader restart does not cancel the durable plan. Reconnect, inspect the layout, and resume waiting for the same operation. A completed plan can be replaced by a later resize, which is why the helper checks its identity.
admin.waitForPartition() waits for serving status active, which can occur while the old copy is still being retired. For draining a group, wait for the whole resize plan to be complete. layout.moves shows each move's phase and retains completed history; a nonempty history does not mean a move is still running.
Shrink back to one group
Reuse admin and waitForResize from above. Keep both groups running while Flower drains east into west:
const shrink = await admin.resize(["west"], {
requestId: "shrink-to-west-001",
});
await waitForResize(shrink.operation);
await admin.removeGroup("east");
console.log(await admin.layout());removeGroup() rejects a group that still owns a partition or has active placement references. It unregisters the group; its processes and files remain under your control. After removal succeeds, take east out of client entry URLs and load-balancer targets, then stop its three server processes. A client entering through west keeps using the same client.partition(name) address throughout.
The designated catalog group cannot be removed. A dedicated catalog can be omitted from the resize target set to drain its named partitions, but its replicas must keep serving the catalog. A group's default database and direct group-targeted transactions are outside partition resizing; account for those separately before retiring servers.
What to expect during a resize
- Each move copies while serving, then freezes for cutover. The partition's code, data, indexes, timers, leases and retry receipts move together. Other partitions keep serving but share CPU, disk and Raft resources.
- Recovery rolls forward. Moves have no cancel operation. Loss of a required group's quorum pauses progress; a frozen partition stays unavailable until the required groups recover. Pending cross-group transactions can also delay its freeze.
- Clients retry and reconnect. Retry uncertain mutations with the same request ID and body. Reconnect live watches through the stable partition URL after an ownership change.
- Finish the current placement work first. A new resize is rejected while another plan, move or partition creation is unfinished. Complete one plan before starting another.
- Balance is by partition count. Size and traffic can differ greatly between partitions. Use
admin.movePartition(name, destination, { requestId })for a deliberate placement when no resize is running. More groups on the same host still share its resources.
See the partition API for the full request and status types, and partition operations for routing and transfer settings.