marketing-service is a Quarkus service and is the most complex service in ShopSTAR3. It owns promotions, coupons, gift cards, product relations, bundles, abandoned cart recovery, internal ad campaigns, external ad platform integrations, and viral/social promotion mechanics. Storage is PostgreSQL with store_id as a discriminator column for multi-tenant isolation. Redis is used for ad frequency caps. External API credentials (Google Ads, Meta, TikTok) are stored in Vault.
Promotions#
Promotions are evaluated by a composable condition-and-action rule engine. They are the primary mechanism for discounts of all kinds, including coupon-based and automatic discounts.
Condition Sets#
Each promotion has a set of conditions that must all be satisfied (AND logic) for the promotion to apply.
| Condition | Description |
|---|---|
cart_total | Cart total falls within a min/max threshold |
customer_group_in | Customer belongs to a specified shopper group |
product_in_cart | A specific variant is present in the cart |
category_in_cart | A product from a specified category is present |
first_order_only | Customer has no prior placed orders |
Action Sets#
Each promotion has one or more actions that execute when all conditions are met.
| Action | Description |
|---|---|
percent_off | Percentage discount applied to the cart total or specific items |
fixed_off | Fixed currency amount removed from the cart total |
free_shipping | Shipping cost set to zero |
BOGO | Buy X units, get Y units free |
add_free_product | A specific variant is added to the cart at zero cost |
Promotion Types and Stacking#
Each promotion record carries:
type:AUTOMATIC(applied at cart display and checkout without any code) orCOUPON_BASED(requires a valid coupon code).priorityINT: determines evaluation order; lower value = evaluated first.stackableBOOLEAN: iffalse, evaluation halts after this promotion is successfully applied, preventing further promotions from stacking.
Coupons#
coupons table: code, promotion_id FK, single_use BOOLEAN, usage_limit INT, usage_count INT.
coupon_usages table: tracks which customer has used which coupon code, enabling per-customer enforcement of single_use restrictions.
Gift Cards#
Gift cards are distinct from store credit. They are pre-funded instruments issued to customers and are owned entirely by marketing-service — the balance is not held in store_credit_accounts in payment-service.
Schema#
gift_cards: code (unique), face_value, currency, store_id, issued_at, expires_at.
gift_card_accounts: current remaining balance per gift card.
gift_card_ledger: append-only movement log. Movement types:
| Movement Type | Trigger |
|---|---|
PURCHASE | Initial load when gift card is activated |
REDEMPTION | Applied at checkout |
REFUND_LOAD | Value restored when associated order is refunded |
EXPIRY | Balance expired at expires_at |
Checkout Integration#
At checkout, checkout-service calls ApplyGiftCard(code, amount, orderId, storeId). If the gift card balance is less than the order total, the net remainder is charged to the customer’s selected payment gateway. If a subsequent checkout step fails, checkout-service calls ReleaseGiftCard(code, amount, orderId) to restore the deducted balance as part of the saga compensation flow.
Product Relations (Upsell and Cross-sell)#
product_relations table: relation_type (UPSELL or CROSS_SELL), source_product_id, target_product_id, store_id, position INT, is_manual BOOLEAN.
The default behavior is rule-based suggestions sourcing products from the same category. Staff can create manual pinned relations via the admin panel. Manual relations (is_manual = true) are always displayed first, ordered by position (lower = higher priority). Rule-based results fill remaining display slots after manual relations are exhausted.
Product relations are exposed via the GraphQL subgraph by extending the federation Product type.
Bundles#
bundles and bundle_items tables define variant sets that carry a percent or fixed discount when purchased together. A bundle is evaluated as a promotion at checkout: if all bundle item variants are present in the cart, the bundle discount is applied automatically.
Abandoned Cart Recovery#
Campaign Structure#
abandoned_cart_campaigns: each campaign defines a multi-step drip sequence. Each step record holds a delay_hours and a message_type (e.g., email reminder, SMS nudge).
Event Flow#
- cart-service publishes a
cart.abandonedevent after a configurable inactivity window. - marketing-service consumes
cart.abandonedand, for each step in the applicable campaign, publishes anotification.requestedevent to communication-service in deferred mode. ThecorrelationIdis set to thecartId; the step number is included in the payload so communication-service can schedule delivery at the correct offset. - When marketing-service consumes an
order.placedevent for a cart that has active recovery steps pending, it publishesnotification.cancelledwithcorrelationId=cartId, which instructs communication-service to cancel all queued recovery notifications for that cart.
abandoned_cart_events table: append-only record of recovery notifications sent and conversions attributed to those sends, used for campaign reporting.
Internal Ads#
Campaign and Variant Structure#
ad_campaigns defines targeting and scheduling for a campaign. ad_variants holds the individual creatives per campaign. Each variant carries:
creative: image URL or text content.traffic_splitNUMERIC: share of traffic allocated to this variant (variants in a campaign sum to 100).is_controlBOOLEAN: marks the baseline variant in an A/B test.
Variant Assignment#
Variant assignment is deterministic: hash(customer_id + campaign_id) % 100. The same customer always sees the same variant for a given campaign, ensuring consistent exposure measurement.
Placement and Audience Targeting#
| Targeting Dimension | Options |
|---|---|
| Placement | homepage_hero, category_banner, product_page, checkout_upsell |
| Audience | Shopper group, device type (MOBILE / DESKTOP), channel (storefront / email) |
Frequency Capping#
Redis is used for frequency caps. The counter key is freq:{customer_id}:{campaign_id}:{window}. The counter is incremented on each impression and checked before serving. If the cap is reached, the campaign is not served to that customer in the current window.
Impression and Click Events#
ad_events table: append-only record of impression and click events, with customer_id, variant_id, placement, and timestamp. Writes are asynchronous to avoid adding latency to the serve path.
External Ads#
Platform Configuration#
external_ad_configs stores per-store, per-platform configuration. Supported platforms: GOOGLE_ADS, META, TIKTOK. Credentials are stored in Vault, not in this table.
Server-Side Conversion Events#
When order.placed is consumed, marketing-service posts a conversion event to the configured platform’s Conversions API. PII (email and phone) is hashed with SHA-256 before transmission.
Customer Audience Sync#
A periodic job exports hashed PII (SHA-256 email and SHA-256 phone) to Google Customer Match and Meta Custom Audiences. When a customer.erasure_requested event is consumed (GDPR right to erasure), a REMOVE record for that customer is sent to all configured platforms.
audience_sync_log tracks the sync status per customer per platform for audit purposes.
Product Catalog Feed#
Catalog events trigger regeneration of Google Merchant Center and Meta Catalog product feeds. The generated feeds are stored as JSON files on S3.
UTM Attribution#
UTM parameters (utm_source, utm_medium, utm_campaign, utm_content, utm_term) are captured at storefront entry and stored per order in the attribution_records table for campaign attribution reporting.
Viral and Social Promotions#
Referral Programs#
referral_programs defines the program terms. referral_codes holds a unique code per customer. referral_conversions is recorded when a referred customer places their first order. Rewards are issued as store credit or a coupon code, depending on the program configuration.
Social Sharing Incentives#
social_share_campaigns defines the campaign. social_share_events records each share and subsequent click. A unique tracking link is generated per customer. A reward is granted upon confirmed engagement (via webhook callback or tracked redirect).
Group and Flash Deals#
group_deals defines the deal and its expiry timer. group_deal_participants tracks enrolled customers. The discount activates only when the min_participants threshold is reached within the expiry window.
Affiliate Tracking#
affiliate_programs and affiliates define the program structure. affiliate_conversions records each attributed sale. Attribution is cookie-based: the affiliate identifier is stored in a browser cookie at click time and read at order placement. Commission is configured as a percentage or fixed amount per conversion. Payouts can be triggered automatically or manually from the admin panel.
Social Shop Integrations#
social_shop_configs holds per-store configuration for Instagram Shopping, Facebook Shop, and TikTok Shop. Catalog synchronization and order ingestion are handled by integration-service. marketing-service owns the platform credentials and configuration records that integration-service reads.
gRPC Interface#
These methods are exposed to checkout-service only.
| Method | Signature | Purpose |
|---|---|---|
EvaluatePromotions | (cartItems[], couponCodes[], customerId, storeId) → PromotionResult | Evaluate all applicable promotions; returns applied discounts and locked line totals |
ApplyGiftCard | (code, amount, orderId, storeId) → GiftCardResult | Deduct from gift card balance; returns actual amount applied |
ReleaseGiftCard | (code, amount, orderId) → ReleaseResult | Saga compensation: restore gift card balance |
ValidateCoupon | (code, storeId, customerId) → ValidationResult | Check code validity, usage limits, and customer eligibility |
Kafka Topics#
Consumed#
| Topic | Action |
|---|---|
order.placed | Loyalty attribution, campaign conversion tracking, server-side ad conversion events, abandoned cart cancellation |
cart.abandoned | Trigger abandoned cart recovery drip sequence |
customer.created | Generate a referral code for the new customer |
Published#
| Topic | Payload |
|---|---|
marketing.recovery_triggered | cartId, storeId, campaignId, step |
marketing.reward_granted | customerId, storeId, rewardType, rewardValue |
marketing.promotion_applied | orderId, storeId, promotionIds[], discountTotal |
notification.requested | Deferred recovery step notification to communication-service |
notification.cancelled | Cancels all pending recovery notifications for a correlationId |
Vault Configuration#
| Key path | Purpose |
|---|---|
ss3/kv/marketing-service/db.url | PostgreSQL JDBC URL |
ss3/kv/marketing-service/db.username | Database username |
ss3/kv/marketing-service/db.password | Database password |
ss3/kv/marketing-service/google-ads.developer-token | Google Ads API developer token |
ss3/kv/marketing-service/google-ads.customer-id | Google Ads customer account ID |
ss3/kv/marketing-service/meta.access-token | Meta Conversions API access token |
ss3/kv/marketing-service/meta.pixel-id | Meta Pixel ID |
ss3/kv/marketing-service/tiktok.access-token | TikTok Events API access token |
ss3/kv/shared/ | Shared Kafka bootstrap and OTel config |