Designing GrowthFlow: A Reference Architecture for Real-Time Growth Experimentation
Growth systems look simple from the outside. Capture a few events, put customers into segments, send a message and compare conversion rates.
The difficult part begins when the platform must answer questions such as:
- Did the customer actually see the experimental treatment?
- Will the same customer always receive the same variant?
- What happens when events arrive twice or out of order?
- Can a customer enter a segment after withdrawing marketing consent?
- Can one tenant consume all available delivery or analytics capacity?
- Is the reported lift statistically valid or merely an artefact of biased assignment?
- May an AI agent propose a campaign without being able to launch it autonomously?
This article presents GrowthFlow, a reference architecture for a multi-tenant growth experimentation platform. It combines real-time event processing, customer profiles, segmentation, experimentation, campaign activation and governed AI assistance.
The design is intended for high-throughput fintech and other regulated environments where conversion matters, but correctness, privacy and auditability cannot be traded away.
Core principle: AI can analyse, explain and recommend. Deterministic services assign treatments, enforce consent, calculate authoritative metrics and execute approved actions.
What GrowthFlow does
Imagine a business onboarding funnel:
- A prospective customer creates an account.
- The customer begins identity or company verification.
- The customer uploads documents.
- Verification succeeds.
- The customer makes a first payment.
GrowthFlow enables a product manager to define a segment such as:
country = "IE"
AND KYC_STARTED occurred
AND KYC_COMPLETED did not occur within 24 hours
AND marketing_consent = true
AND messages_sent_last_7_days < 1
The product manager can create an experiment:
- Control: no intervention
- Variant A: send an educational verification email
- Variant B: show a simplified document checklist
The platform assigns customers consistently, records real exposure, activates the approved treatment and measures verification completion within an attribution window.
An AI growth analyst can then inspect aggregated funnel and experiment data, identify a meaningful drop-off and recommend the next hypothesis. It cannot change assignment, bypass consent or activate a campaign.
Goals and boundaries
Goals
- Ingest behavioural and business events reliably.
- Maintain explainable customer profile projections.
- Evaluate real-time and batch segments.
- Assign customers to experiments deterministically.
- Record exposure and conversion without double counting.
- Activate approved campaigns through external providers.
- Support multiple tenants without noisy-neighbour failures.
- Provide evidence-backed AI analysis.
- Preserve auditability, privacy and regulatory controls.
Non-goals for the first release
- A complete marketing automation suite.
- A general-purpose data warehouse.
- Autonomous AI campaign deployment.
- Cross-device identity resolution based on probabilistic tracking.
- Arbitrary user-authored code inside segment definitions.
- Replacing a product analytics platform on day one.
Architectural principles
- Events are facts, not mutable state. Corrections arrive as new events.
- Assignment and exposure are different. A customer can be assigned without seeing a treatment.
- Consent is checked at execution time. Eligibility at segment-entry time is insufficient.
- Every asynchronous operation is idempotent. Delivery is assumed to be at least once.
- Tenant scope comes from verified identity. It is never trusted from an arbitrary request field.
- Analytical projections are rebuildable. Authoritative configuration and audit state remain transactional.
- The LLM is not a workflow engine. Durable workflows own retries, timeouts and state transitions.
- Start modular. Split services when scaling, security or ownership creates evidence for a boundary.
High-level architecture
flowchart TB
C[Web, mobile and partner clients] --> E[Edge and event API]
E --> K[Kafka event backbone]
K --> P[Customer profile and segmentation]
K --> X[Experiment and metric processing]
P --> A[Activation workflow]
X --> W[Analytics warehouse]
A --> M[Email, webhook and product channels]
W --> AI[AI growth analyst]
AI --> G[Governed read-only tools]
G --> W
The platform is separated into three planes:
| Plane | Responsibilities | Primary technology |
|---|---|---|
| Control plane | Tenancy, identity, experiment definitions, segments, approvals, consent policy, audit and workflow state | Java 21 and Kotlin with Spring Boot and PostgreSQL |
| Streaming data plane | Event ingestion, validation, profile projections, segment transitions, exposure and conversion processing | Kafka, Kafka Streams, Java 21; optional Go collector |
| Intelligence plane | Funnel analysis, experiment interpretation, hypothesis generation and AI evaluation | Python, Bedrock-compatible models and governed tools |
Why use more than one language?
Polyglot architecture is useful only when ownership remains clear.
Java 21
Java owns the authoritative platform:
- Event contracts and validation
- Customer profiles
- Segment compilation and evaluation
- Experiment assignment
- Exposure and conversion handling
- Consent and suppression policies
- Tenant authorization
- Audit and usage metering
Java 21 provides a mature Spring ecosystem, strong Kafka support and predictable operational behaviour. Records and sealed interfaces work well for typed commands and events. Virtual threads can simplify selected blocking integrations, but should be introduced only after measurement.
Kotlin
Kotlin is used for one meaningful bounded component rather than for decorative polyglotism. The activation orchestrator is a good candidate because sealed classes, null safety and coroutines make workflow adapters concise.
The Java and Kotlin code remains interoperable and follows the same API and event contracts.
Python
Python owns model orchestration and evaluation:
- AI growth-analysis agent
- Typed tool invocation
- Prompt and model routing
- Offline evaluation
- Statistical exploration and notebooks
Python receives no direct PostgreSQL credentials for authoritative product state. It reads data through scoped tools and submits recommendations as proposals.
Go
Go is optional and intentionally narrow. It can power a lightweight, high-concurrency public event or webhook collector where fast startup and predictable memory use are useful. It does not own customer rules, experiment decisions or legal consent policy.
1. Event ingestion
The event API accepts browser, mobile, backend and partner events.
{
"eventId": "evt-8291",
"schemaVersion": 3,
"tenantId": "resolved-from-authentication",
"customerId": "cus-184",
"anonymousId": null,
"eventType": "KYC_STARTED",
"occurredAt": "2026-09-03T14:30:00Z",
"receivedAt": "2026-09-03T14:30:02Z",
"source": "ONBOARDING_SERVICE",
"attributes": {
"country": "IE",
"companyType": "LIMITED_COMPANY",
"acquisitionChannel": "PAID_SEARCH"
}
}
Ingestion sequence
sequenceDiagram
participant Client
participant API as Event API
participant Kafka
participant Profile as Profile Projector
participant Segment as Segment Engine
Client->>API: POST event + idempotency key
API->>API: Authenticate, validate, enrich
API->>Kafka: Publish accepted event
API-->>Client: 202 Accepted
Kafka->>Profile: Update customer projection
Profile->>Kafka: CustomerProfileChanged
Kafka->>Segment: Evaluate affected segments
Segment->>Kafka: SegmentMembershipChanged
Partitioning
Events that change a customer profile are partitioned by:
tenantId + ":" + customerId
This preserves per-customer processing order without pretending that Kafka provides a global order.
Idempotency
The platform deduplicates by tenantId + eventId. The event remains safe when:
- The client retries after a timeout.
- Kafka redelivers.
- A consumer restarts after processing but before committing its offset.
- A provider sends the same callback repeatedly.
Deduplication records should have a retention period that matches realistic retry and replay windows. Business handlers must still be idempotent because a cache-based deduplication layer is not an absolute guarantee.
Late and out-of-order events
occurredAt represents business time; receivedAt represents platform arrival time. Projections define a bounded lateness policy.
For example:
- Update the live profile immediately.
- Recompute affected attribution windows when the late event is still within policy.
- Emit an adjustment event instead of mutating an earlier analytical fact.
- Send excessively late or invalid events to a review stream with a reason code.
Schema evolution
Use Avro or Protobuf with a schema registry. Apply backward-compatible changes by default:
- Add optional fields with defaults.
- Never silently change a field’s meaning.
- Introduce a new event type for incompatible business semantics.
- Test producers and consumers against registered schemas in CI.
2. Customer profile projection
The customer profile is a rebuildable view derived from events and authoritative customer services.
{
"tenantId": "tnt-1",
"customerId": "cus-184",
"country": "IE",
"companyType": "LIMITED_COMPANY",
"lifecycleStage": "KYC_IN_PROGRESS",
"marketingConsent": true,
"kycStartedAt": "2026-09-03T14:30:00Z",
"kycCompletedAt": null,
"firstPaymentAt": null,
"messagesSentLast7Days": 0,
"profileVersion": 83
}
The projection stores source metadata for explainability. A user should be able to ask why the profile says KYC_IN_PROGRESS and see the events or authoritative fields that produced it.
Identity merge
Anonymous-to-authenticated identity is handled explicitly:
- Events initially use an anonymous identifier.
- Authentication emits an
IdentityLinkedevent. - A deterministic policy merges eligible history.
- Sensitive or consent-restricted events remain excluded.
- The merge emits an auditable profile version.
Probabilistic identity stitching is outside the first scope.
3. Segment engine
Product managers should define segments through a constrained domain-specific language rather than SQL or arbitrary code.
segment:
name: "Irish KYC drop-offs"
version: 4
all:
- equals: { field: country, value: IE }
- event_occurred: { type: KYC_STARTED }
- event_not_occurred_within:
type: KYC_COMPLETED
after: KYC_STARTED
duration: PT24H
- equals: { field: marketingConsent, value: true }
- less_than: { field: messagesSentLast7Days, value: 1 }
Compilation
The Java segment service validates and compiles this definition into an internal expression tree:
sealed interface Predicate permits Equals, LessThan, EventOccurred,
EventNotOccurredWithin, All, Any, Not {}
record Equals(String field, JsonNode value) implements Predicate {}
record All(List<Predicate> predicates) implements Predicate {}
The runtime never asks an LLM whether a customer belongs to a segment. The compiled rule executes deterministically.
Incremental evaluation
Each event type maps to potentially affected profile fields and segments. A KYC_COMPLETED event should not cause every segment in the system to be evaluated.
Maintain an index such as:
changed field/event -> candidate segment versions
The engine evaluates only candidates, compares the result with existing membership and emits a transition when membership changes.
{
"eventType": "SEGMENT_MEMBERSHIP_CHANGED",
"tenantId": "tnt-1",
"segmentId": "seg-kyc-dropoff",
"segmentVersion": 4,
"customerId": "cus-184",
"change": "ENTERED",
"reasonCodes": ["COUNTRY_IE", "KYC_INCOMPLETE_24H", "CONSENT_VALID"],
"evaluatedAt": "2026-09-04T14:30:00Z"
}
Temporal conditions
Conditions such as “did not complete within 24 hours” require a timer or delayed evaluation. Options include:
- A durable workflow timer
- A time-indexed scheduler
- A Kafka Streams punctuator with durable state
- A delayed queue abstraction
For business-critical activation, a durable workflow system is easier to inspect and recover than ad hoc application timers.
4. Experiment service
The experiment service owns definitions, allocation and eligibility. It does not own analytical conclusions.
Experiment definition
{
"experimentId": "exp-kyc-checklist-01",
"version": 2,
"status": "RUNNING",
"randomizationUnit": "CUSTOMER",
"eligibilitySegment": {
"segmentId": "seg-kyc-dropoff",
"version": 4
},
"variants": [
{ "id": "control", "startBucket": 0, "endBucket": 4999 },
{ "id": "checklist", "startBucket": 5000, "endBucket": 9999 }
],
"primaryMetric": "KYC_COMPLETED_WITHIN_48H",
"guardrailMetrics": ["SUPPORT_CONTACT_RATE", "COMPLIANCE_REJECTION_RATE"]
}
Stable assignment
Assignment must be deterministic:
public Variant assign(UUID experimentId, String customerId,
List<VariantAllocation> allocations) {
String key = experimentId + ":" + customerId;
long hash = Integer.toUnsignedLong(murmur3_32(key));
int bucket = (int) (hash % 10_000);
return allocations.stream()
.filter(a -> bucket >= a.startInclusive())
.filter(a -> bucket < a.endExclusive())
.findFirst()
.map(VariantAllocation::variant)
.orElseThrow();
}
Properties worth testing:
- The same customer and experiment always produce the same bucket.
- Allocation ranges do not overlap.
- Every enabled bucket maps to exactly one variant.
- Distribution remains within tolerance over a large generated population.
- Changing unrelated customer attributes cannot change assignment.
Assignment is not exposure
A customer is assigned when the platform selects a variant. The customer is exposed only when the treatment is actually delivered or rendered.
This distinction prevents customers who never saw a treatment from diluting experimental results.
An exposure event should include:
- Experiment and version
- Variant
- Customer and tenant
- Assignment unit
- Channel
- Timestamp
- Surface or campaign version
- Unique exposure ID
Mutual exclusion
Two experiments can interfere with one another. Place sensitive experiments into exclusion groups and allocate a customer to only one experiment in the group.
The group assignment occurs before individual experiment assignment and uses the same stable bucketing principle.
5. Metric and attribution processing
GrowthFlow separates immutable facts from computed aggregates.
Facts
- Assignment
- Exposure
- Conversion event
- Suppression
- Delivery attempt and outcome
- Experiment configuration version
Aggregates
- Exposed population by variant
- Conversion count by variant
- Conversion rate
- Absolute and relative lift
- Confidence interval
- Funnel-stage transition
- Time to conversion
- Guardrail metric changes
Attribution window
For each conversion metric, define:
- Qualifying exposure
- Eligible conversion event
- Minimum and maximum delay
- First-touch or last-touch behaviour
- Deduplication key
- Exclusion criteria
Example:
metric:
name: KYC_COMPLETED_WITHIN_48H
exposure: KYC_CHECKLIST_RENDERED
conversion: KYC_COMPLETED
window: PT48H
aggregation: UNIQUE_CUSTOMERS
deduplicate_by: tenantId+experimentId+customerId
Statistical correctness
A production experimentation platform must address:
- Statistical power and minimum detectable effect
- Sample-ratio mismatch
- Confidence intervals
- Repeated peeking and sequential testing
- Multiple comparisons
- Novelty and seasonality
- Practical significance
- Guardrail regressions
The MVP can start with fixed-horizon analysis and a predeclared sample size. It should avoid presenting a continuously changing p-value as a green “winner” indicator.
Analytical store
Operational definitions belong in PostgreSQL. High-volume events and aggregates belong in an analytical store such as BigQuery or ClickHouse.
flowchart LR
K[Kafka] --> S[Stream processors]
S --> O[Operational projections]
S --> W[BigQuery or ClickHouse]
W --> D[Experiment dashboard]
W --> T[Read-only AI tools]
6. Activation workflow
Activation connects segment transitions and experiment variants to customer-facing channels.
stateDiagram-v2
[*] --> Eligible
Eligible --> Suppressed: consent or frequency rule fails
Eligible --> Assigned: experiment assignment
Assigned --> Scheduled
Scheduled --> Sending
Sending --> Delivered
Sending --> RetryableFailure
RetryableFailure --> Sending
RetryableFailure --> Failed
Delivered --> Converted
Delivered --> WindowExpired
Why use durable orchestration?
Marketing activation contains long waits, external callbacks and human approvals. A durable workflow engine such as Temporal can model:
- Wait until the scheduled time.
- Recheck consent and suppression.
- Resolve the approved template.
- Call the provider with an idempotency key.
- Retry transient failures.
- Reconcile an ambiguous timeout.
- Wait for a delivery callback.
- Observe conversion until the window closes.
Kafka remains the event backbone, but it does not need to encode every workflow transition through choreography.
Provider adapter contract
interface ActivationProvider {
suspend fun send(command: SendCommand): SendResult
suspend fun reconcile(providerMessageId: String): DeliveryStatus
}
sealed interface SendResult {
data class Accepted(val providerMessageId: String) : SendResult
data class Rejected(val reason: String) : SendResult
data class Unknown(val correlationId: String) : SendResult
}
An Unknown result is important. A network timeout does not prove the provider rejected the message. Blind retrying can send duplicate communications.
Rate limiting and fairness
Use hierarchical limits:
- Global provider limit
- Tenant/provider limit
- Campaign limit
- Customer frequency limit
A Redis Lua script can apply an atomic token-bucket policy. Durable usage and delivery history remains outside Redis.
7. AI growth analyst
The AI component is an analyst and hypothesis assistant, not an execution authority.
Allowed tools
| Tool | Purpose | Access mode |
|---|---|---|
get_funnel_metrics |
Read funnel counts and rates | Aggregated read |
compare_experiment_variants |
Retrieve experiment metrics and uncertainty | Aggregated read |
inspect_segment_definition |
Explain an immutable segment version | Configuration read |
find_dropoff_patterns |
Search approved dimensions for material changes | Aggregated read |
get_previous_experiments |
Retrieve prior approved summaries | Read |
estimate_segment_size |
Return approximate audience size | Bounded read |
save_hypothesis_proposal |
Store a draft recommendation | Draft-only command |
There is deliberately no launch_campaign, change_allocation or unrestricted SQL tool.
Agent flow
flowchart TB
Q[Growth question] --> P[Plan analysis]
P --> R[Retrieve approved metrics]
R --> V[Validate data sufficiency]
V -->|Insufficient| H[Ask a focused question]
V -->|Sufficient| A[Analyse pattern]
A --> E[Check experiment history]
E --> D[Draft hypothesis and metrics]
D --> C[Human review]
Example response contract
{
"observation": "Paid-search mobile users in Ireland complete KYC less often",
"evidence": [
{
"metricId": "funnel-kyc-ie-mobile-paid",
"period": "2026-08-01/2026-08-31",
"difference": -0.14
}
],
"hypothesis": "Showing a contextual document checklist will reduce uncertainty",
"proposedExperiment": {
"primaryMetric": "KYC_COMPLETED_WITHIN_48H",
"guardrails": ["SUPPORT_CONTACT_RATE", "COMPLIANCE_REJECTION_RATE"]
},
"assumptions": ["Channel attribution is complete for the selected cohort"],
"requiresHumanApproval": true
}
AI controls
- Pydantic validation for model and tool boundaries
- Tenant-scoped workload identity
- Read-only analytical views
- Maximum tool calls, tokens and wall-clock time
- Prompt and model versioning
- No raw personal data unless explicitly needed and permitted
- Evidence references in every quantitative claim
- Offline evaluation before model or prompt changes
- Human approval for every customer-facing action
8. Data model
The core PostgreSQL model includes:
| Entity | Important invariants |
|---|---|
| Tenant | Region, plan, quotas and privacy policy are versioned |
| Customer reference | Tenant-scoped identifier; sensitive master data can remain in source systems |
| Segment version | Immutable after publication |
| Segment membership | References exact segment and profile versions |
| Experiment version | Immutable while running; allocation changes create a new version |
| Assignment | Unique per experiment version and randomisation unit |
| Exposure | Immutable, idempotent fact tied to the delivered surface |
| Metric definition | Versioned exposure, conversion and attribution rules |
| Campaign | Binds segment, experiment variant, template and channel |
| Activation attempt | Records idempotency, provider result and reconciliation status |
| Consent decision | Captures policy input and result at execution time |
| Audit event | Append-only record of configuration and approval changes |
Analytical tables are optimised separately for cohort and funnel queries.
9. APIs
Public and internal endpoints
POST /v1/events
POST /v1/segments
POST /v1/segments/{id}/versions
POST /v1/experiments
POST /v1/experiments/{id}/start
POST /v1/experiments/{id}/stop
GET /v1/experiments/{id}/results
GET /v1/customers/{id}/segments
GET /v1/customers/{id}/assignments
POST /v1/campaigns
POST /v1/campaigns/{id}/approve
POST /v1/ai/analyses
Configuration-changing commands require:
- Verified tenant and actor
- Expected resource version
- Idempotency key
- Policy decision
- Audit reason for sensitive actions
10. Domain events
| Event | Producer | Important consumers |
|---|---|---|
CustomerEventAccepted |
Event API | Profile projector, warehouse sink |
CustomerProfileChanged |
Profile service | Segment engine |
SegmentMembershipChanged |
Segment engine | Experiment eligibility, activation |
ExperimentAssigned |
Experiment service | Product surface, audit |
ExperimentExposed |
Product/activation channel | Metric processor |
ConversionObserved |
Metric processor | Experiment aggregates |
ActivationRequested |
Campaign service | Durable activation workflow |
ActivationDelivered |
Provider adapter | Metrics, customer history |
ActivationSuppressed |
Consent/frequency policy | Audit, campaign reporting |
ExperimentAnalysisUpdated |
Analytics pipeline | Dashboard, AI tools |
Events use at-least-once delivery. Consumers maintain idempotency and never depend on global ordering.
11. Multi-tenancy and security
Tenant isolation
- Derive tenant context from an authenticated token or signed workload envelope.
- Include
tenant_idin relational primary and foreign-key strategies. - Require a tenant context in every repository interface.
- Use PostgreSQL Row Level Security as defence in depth.
- Filter analytics and stream state by tenant.
- Run synthetic cross-tenant canary tests continuously.
- Prevent the AI model from selecting or changing its tenant scope.
Consent and privacy
- Keep purpose-specific consent, not a single generic Boolean.
- Recheck consent immediately before activation.
- Apply suppression and frequency rules after assignment but before delivery.
- Minimise personal data in Kafka, logs and analytics.
- Support retention, deletion and regional-residency policies.
- Avoid using customer content for model improvement without explicit contractual permission.
Threats
| Threat | Control |
|---|---|
| Forged tenant ID | Identity-derived scope; ignore client-supplied tenant selection |
| Duplicate customer contact | End-to-end idempotency and provider reconciliation |
| Segment rule abuse | Constrained DSL, complexity limit and approval workflow |
| Experiment manipulation | Immutable running version and audited allocation |
| Prompt injection | Treat campaign text and warehouse strings as untrusted data; governed tools |
| Cross-tenant AI query | Scoped views, workload identity and post-query validation |
| Sensitive logging | Structured safe logging and prohibited-field tests |
12. Reliability and scaling
Initial scale assumptions
- 25 tenants
- 10 million customer events per day
- 500 peak ingestion requests per second
- 100,000 active experiment assignments
- 50 concurrent analytical or AI queries
- Multiple providers with independent quotas
Scaling strategy
- Scale ingestion independently from profile and analytical processing.
- Partition customer-state streams by tenant and customer.
- Maintain separate workload classes for interactive APIs, backfills and campaign bursts.
- Enforce tenant quotas before work enters constrained downstream systems.
- Scale consumers on queue lag and event age, not CPU alone.
- Use backpressure rather than unlimited producer retries.
- Make replay a first-class operational operation.
Failure behaviour
| Failure | Response |
|---|---|
| Kafka consumer crash | Resume from committed offset; idempotent handler prevents duplicate effect |
| Warehouse delay | Continue operational assignment; mark analytics freshness visibly |
| Provider timeout | Reconcile before retrying an ambiguous send |
| Consent service unavailable | Fail closed and delay activation |
| Experiment service unavailable | Use a short-lived, versioned assignment cache only when policy allows |
| AI model unavailable | Disable recommendations; core experimentation continues |
| Poison event | Move to DLQ with safe metadata and support replay tooling |
13. Observability
One trace should connect:
Event accepted
-> profile updated
-> segment entered
-> experiment assigned
-> activation approved
-> message delivered
-> exposure recorded
-> conversion attributed
Metrics
- Ingestion requests, rejection rate and P95 latency
- Kafka lag and oldest-event age
- Profile projection latency
- Segment evaluation rate and duration
- Assignment distribution by variant
- Sample-ratio mismatch alerts
- Activation attempt, delivery and suppression rates
- Provider latency and error rate
- Conversion-processing delay
- Analytical freshness
- AI tool calls, latency, token cost and evaluation score
- Per-tenant resource consumption
Do not place personal attributes, campaign content or raw prompts into trace attributes.
14. Testing strategy
Deterministic platform tests
- Unit tests for segment predicates and allocation ranges
- Property-based tests for stable and balanced assignment
- Schema compatibility tests
- Kafka consumer idempotency tests
- Provider timeout and reconciliation tests
- PostgreSQL tenant-isolation tests
- Contract tests between Java, Kotlin, Python and Go components
- Replay tests against captured synthetic event streams
Experimentation tests
- Synthetic balanced populations
- Intentional sample-ratio mismatch
- Missing exposure events
- Duplicate conversions
- Events outside attribution windows
- Late and out-of-order conversions
- Mutually exclusive experiment conflicts
AI evaluation
- Correct use of approved tools
- Quantitative claims linked to returned metrics
- No invention of unavailable dimensions
- Appropriate request for missing context
- Useful hypothesis and measurable primary metric
- Inclusion of guardrail metrics
- No attempt to invoke prohibited actions
15. Deployment
AWS-first option
- EKS or ECS for Java/Kotlin and Go workloads
- Amazon MSK for Kafka
- Aurora PostgreSQL
- ElastiCache for rate limiting and bounded caches
- S3 for batch artefacts
- Bedrock/AgentCore-compatible runtime for the AI analyst
- CloudWatch plus OpenTelemetry/Grafana
GCP option
- GKE or Cloud Run
- Managed Kafka provider or Pub/Sub adapters where ordering semantics fit
- Cloud SQL or AlloyDB
- Memorystore
- BigQuery for analytics
- Vertex AI or external model adapters
- Cloud Monitoring with OpenTelemetry
The domain architecture should not depend on one cloud’s event envelope or model API. Cloud-specific capabilities remain behind adapters.
16. Repository structure
/services
/growth-platform-java
/event-api
/customer-profile
/segmentation
/experimentation
/activation-kotlin
/event-collector-go
/growth-agent-python
/contracts
/openapi
/events
/tools
/schemas
/analytics
/warehouse
/metrics
/dashboards
/evaluation
/datasets
/scorers
/red-team
/infra
/modules
/environments
/policies
/docs
/adrs
/runbooks
/threat-model
Start with one modular Java deployment, one Kotlin activation worker, one Python agent and an optional Go collector. The repository may contain clear module boundaries without requiring a separately deployed microservice for every noun.
17. MVP roadmap
MVP 0: walking skeleton
Deliver in approximately two weeks:
- Authenticated event ingestion
- Kafka topic and schema
- Customer profile projection
- One fixed segment
- One two-variant experiment
- Exposure and conversion facts
- Basic result endpoint
- End-to-end trace
MVP 1: credible portfolio demo
- Versioned segment DSL
- Stable assignment and mutual exclusion
- Funnel dashboard
- Fixed-horizon conversion calculation
- Email or webhook activation
- Consent and frequency checks
- Provider retry and reconciliation
- Tenant isolation
- Load test and Grafana dashboard
MVP 2: production foundation
- Durable activation workflows
- Warehouse-backed analytics
- Experiment approval lifecycle
- Dynamic quotas and fair scheduling
- SSO and enterprise roles
- Backfill and replay tooling
- Retention and deletion workflows
- Operational runbooks and SLOs
MVP 3: AI differentiation
- Read-only AI growth analyst
- Historical experiment retrieval
- Structured hypothesis proposals
- Statistical-data sufficiency checks
- Prompt/model release evaluation
- Human approval workflow
- Token and cost budgets
18. Trade-offs and rejected alternatives
Why not let the LLM evaluate segments?
It would be expensive, slow, difficult to reproduce and unsafe for consent-sensitive decisions. The segment DSL is deterministic. AI may help a product manager draft a segment, but the compiled rule must be reviewed and validated.
Why not store everything in the warehouse?
Warehouses are excellent for analytics but are not ideal as the only source of transactional workflow, approval and idempotency state. PostgreSQL owns operational truth; the warehouse owns analytical projections.
Why not make Kafka the workflow engine?
Kafka is excellent for facts and decoupled stream processing. Long waits, human approvals, ambiguous provider responses and compensating actions are easier to manage in a durable workflow abstraction.
Why not make every component a microservice?
Early service proliferation increases network failure modes, deployment overhead and contract-management cost. Modular boundaries allow later extraction based on load, security or team ownership.
Why separate exposure from assignment?
Because measuring assigned customers who never saw the treatment biases the experiment toward no effect. Assignment remains necessary for deterministic treatment; exposure is necessary for meaningful analysis.
19. Interview discussion points
This architecture supports several useful system-design conversations:
- How would you preserve deterministic assignment across regions?
- How would you detect and respond to sample-ratio mismatch?
- How do you handle a provider timeout without sending twice?
- What happens when a customer withdraws consent after entering a segment?
- How do you rebuild profile and segment state from Kafka?
- How would you prevent one tenant from exhausting provider capacity?
- Which state belongs in PostgreSQL, Kafka Streams and the warehouse?
- When should choreography become durable orchestration?
- How can an AI agent help without gaining execution authority?
- How do you measure correctness independently from availability?
Conclusion
A growth platform is not merely an event API attached to an email provider. It is a distributed decision system whose outputs affect customers, revenue, privacy and the validity of product decisions.
GrowthFlow keeps the most important responsibilities explicit:
- Java and Kotlin own deterministic business behaviour.
- Kafka owns the durable stream of behavioural facts.
- PostgreSQL owns transactional configuration and audit state.
- The analytical store owns cohort and experiment queries.
- Durable workflows own long-running activation.
- Python and the LLM provide governed analysis rather than autonomous execution.
That separation makes rapid experimentation possible without sacrificing correctness or control—the central engineering tension in modern growth systems.
References
- Airwallex engineering and culture stories
- Airwallex: Tucking In Your Legacy Tech Debt With Temporal
- Airwallex: Interactive Business Process With Temporal
- Apache Kafka documentation
- Temporal documentation
GrowthFlow is a reference architecture and portfolio design, not an Airwallex product or an account of Airwallex’s internal Growth platform.
