Back to releases

Scale and Reliability

Pomavo now runs background work through Kafka and MongoDB, executes DSL mutations in distributed batches, protects ticket fields with locks, and persists board preferences per user.

Scale and Reliability is about making large work feel ordinary. You can run bulk mutations, process automations, preserve ticket governance, and keep boards personalized while Pomavo moves side effects through durable background infrastructure.

Durable jobs and distributed mutation batches make large changes inspectable.

Kafka becomes the job backbone

Pomavo moved job delivery to Kafka so background work is fed by a durable, partitioned log. A user action still records the core database change first, then the side effects move through the Job Pipeline: indexing, history, inbox notifications, attachment tracking, automations, and scheduler resumes.

Kafka gives Worker replicas a shared consumer group. Work is balanced across partitions, offsets are committed after successful processing, and partitions preserve order where the partition key requires it. This keeps related work predictable while allowing unrelated jobs to run in parallel.

Pipeline stageWhat happens
API eventThe Web API persists the user action and emits a typed event
Job recordPomavo creates a durable job record you can inspect
Kafka publishThe job event is published to the job topic
Worker feederA Kafka feeder consumes events in the worker group
Partition laneEach partition is processed sequentially with its own channel
Handler executionThe orchestrator resolves the correct handler and records a run
Sub-job fan-outHandlers create independently tracked child jobs

The result is a pipeline that can absorb more work without turning every API request into a synchronous chain of side effects.

MongoDB stores jobs and run logs

Jobs and run logs moved to MongoDB because their shape is flexible. A job needs a payload, status, parent and child relationships, per-run state, result data, and many log lines that differ by handler type.

This storage model pairs well with the Admin Job Explorer. You can open a job, see each run, inspect status transitions, read the full log, and follow child jobs created by a parent operation.

Job dataWhy document storage fits
PayloadDifferent job types carry different JSON shapes
RunsA job can be retried, requeued, succeeded, errored, or terminated
Log linesHandlers record detailed progress and diagnostics
Child jobsFan-out creates a tree of related work
ResultsEach handler returns type-specific summary data

The job record is the durable unit of work. The run log is the evidence trail you use when you need to know exactly what happened.

Worker resilience and startup readiness

The Kafka feeder now has resilience built into the worker startup path. It waits behind a startup readiness gate, connects to Kafka, marks the Kafka consumer prerequisite ready, and retries connection failures with a bounded backoff before marking the prerequisite failed.

Once running, the feeder creates a processing lane per assigned partition. If a lane buffer fills, the feeder pauses that partition while continuing to service others. When the buffer drains, the partition resumes. This gives you backpressure without stopping the whole worker.

Resilience behaviorReliability effect
Startup gateThe worker does not report ready before Kafka consumption is established
Manual offsetsOffsets are committed after each message is processed
Partition pause and resumeHot partitions stop flooding full internal queues
Dead letter topicUnexpected processing failures can be routed away from the main stream
Health loggingLong-running jobs and queue depth are visible in logs
Bounded reconnect loopTransient Kafka failures retry before the worker gives up

Distributed DSL mutations

Bulk DSL mutations are now asynchronous and distributed. When you run a mutation from the Query Language, Pomavo creates a coordinator job. The coordinator parses the query, validates that mutation clauses are present, collects matching tickets, and splits the work into batch sub-jobs.

dsl
template = "Bug" and priority = "Critical" and status != "Done"
set status = "In Progress", assignee = @me

A standalone create mutation runs inline in the coordinator because there is no source-ticket set to fan out. Mutations that operate on existing tickets use batch sub-jobs so Worker replicas can process slices in parallel.

Parse and validate

The coordinator tokenizes and parses the DSL, then rejects non-mutation queries or invalid mutation filters.

Collect matches

It queries the search index for every matching source ticket, respecting an explicit limit when present.

Create batches

It chunks the matches into batches of 100 tickets.

Dispatch sub-jobs

Each batch gets a partition key based on the parent job and batch index so work spreads across stream partitions.

Aggregate results

Each batch logs update, create, link, unlink, comment, success, failure, throughput, and error counts.

Search_after pagination for large result sets

The coordinator uses search_after pagination instead of offset pagination when collecting tickets for a mutation. That matters for large result sets because Elasticsearch can page forward using a cursor rather than repeatedly skipping from the beginning.

The implementation fetches pages of 1000 results, stores the returned cursor, and continues until it reaches the query limit, an empty page, or a missing cursor. The log records page count, total tickets found, and collection time so you can see whether time was spent searching or executing.

Collection detailValue
Page mechanismsearch_after cursor
Fetch page size1000 tickets
Batch chunk size100 tickets per mutation sub-job
Limit handlingStops at the query limit when provided
LoggingPage count, total tickets, and elapsed time

This is the kind of plumbing you rarely see in the product, but you feel it when a large mutation completes as background work instead of freezing the browser.

Caller impersonation in async mutations

Async work still runs as you. The mutation coordinator and each batch sub-job set the organization context and user context before calling domain services. That caller impersonation keeps permission checks, audit behavior, tenant scope, and business rules aligned with the original request.

This mirrors the security approach in the Security Model: the UI, API, query mutations, and automations all go through the same permission-gated services. Moving work to Kafka does not create a privileged shortcut.

ContextPreserved for
Organization idTenant scoping across search, repositories, and services
User idPermission checks and user-attributed changes
Query textReparse and execute the exact mutation in every batch
Source ticket snapshotApply clauses to the intended set of tickets
Parent job idLink sub-jobs back to the originating mutation

Bulk mutations still affect every matching ticket. Run the DSL as a plain search first, then add the mutation clause when you are confident in the result set.

Field locks protect work in flight

Ticket Locks now give you hard governance controls for specific parts of a ticket. A lock can prevent field edits, link changes, attachment changes, status transitions, or new comments until an unlock path runs.

Lock typePrevents
FieldEditing a specific field value
LinkAdding or removing ticket links
AttachmentAdding or removing attachments
Status TransitionMoving the ticket to another workflow state
Comment AddPosting new comments

Locks are enforced whether a change comes from a person in the UI or from automation. That makes them useful for release freeze rules, approval gates, controlled transitions, and audit-sensitive records.

Multi-user fields and avatar stacks

Multi-user fields now render as overlapping avatar stacks. You can scan contributors, reviewers, watchers, or shared owners quickly without turning every card into a long list of names.

The visual pattern is small, but it affects boards and ticket previews every day. Dense team fields remain readable, while hover and detail views can still provide the full user context when you need it.

UI patternWhy it helps
Overlapping avatarsShows several users in compact space
Consistent orderingKeeps the stack stable as cards refresh
Overflow handlingAvoids stretching cards for large groups
Shared field renderingThe same multi-user field can appear across templates

DSL functions and aggregations graduate into dedicated projects

The DSL implementation was extracted into dedicated Pomavo.Dsl and Pomavo.Dsl.Elasticsearch projects. That separation makes the language easier to test, reuse, and evolve without entangling parser logic with API controller code.

The user-facing language still supports filters, sorting, pagination, references, date literals, bulk mutations, and aggregation. What changed is maintainability: functions and aggregations can now be developed as a focused language layer.

dsl
group by assignee
return assignee, count() as open_tickets, sum(
quot;Story Points"
) as points order by points desc
DSL capabilityExamples
Date functionsnow(), today(), startOfDay(date), startOfWeek(date)
Value functionslength(value)
Aggregationscount(), sum(expr), avg(expr), min(expr), max(expr)
References@me, @username, $field,
quot;Field Name"
Mutationsset, create, link, unlink, comment

Persisted board preferences

Project boards now remember per-user view preferences. You can return to the board with your chosen presentation intact rather than rebuilding the same working view every session.

This is deliberately user-scoped. Your board density, grouping, filtering, or display choices do not force the same view onto teammates. Each person gets a board that stays close to how they work.

Preference behaviorProduct effect
Saved per userYour board setup follows your account
Scoped to board contextPreferences do not leak across unrelated places
Restored on returnYou spend less time resetting the UI
Compatible with saved queriesTeam-level saved searches and personal view state can coexist

Load tests and operational confidence

The k6 load-test suite gives you a repeatable way to exercise the scale work. It is especially valuable for paths where user actions trigger background jobs, search queries, and worker fan-out.

The goal is confidence under realistic pressure: the API should stay responsive, the worker should keep draining Kafka partitions, MongoDB logs should remain inspectable, and admin views should show the run tree when something slows down.

Durable delivery

Kafka carries jobs through a partitioned log with worker consumer groups.

Inspectable storage

MongoDB stores flexible job payloads, runs, results, and logs.

Distributed mutations

Large DSL mutations split into 100-ticket batch sub-jobs.

Governed tickets

Locks protect fields, links, attachments, transitions, and comments.

Next, this reliability foundation gives you room for larger automations, richer reports, and more integration work while preserving the same tenant and permission boundaries.