Skip to content

Phoenix microservices

Anna Zubenko edited this page May 11, 2017 · 5 revisions

This document is intended as a tech spec of phoenix microservices split out of phoenix and hopefully a dev guide for migrating or creating new ones.

Motivation

There are several key points to splitting phoenix:

  • API contract between phoenix and microservice acts as spec and documentation
  • we can test (and performance test!) pieces individually
  • switch to new microservice version can be performed only when we're sure everything works as expected
  • implementation can be chosen dynamically for every tenant (allowing us to have different independent implementations of varying complexity, and even allowing customers to have their own implementations against the given API contract)
  • less code to build, transfer over network and deploy
  • less accidentally tangled code in services

Possible future of phoenix

Phoenix becomes an orchestration layer

  • performing calls to microservices
    • sequentially or in parallel
    • caching results
    • picking appropriate implementation for the tenant who initiated request
    • composing response entities and failures from the collected data
  • making no database queries
  • providing a unified API and proxying requests to all microservices
  • pushing data to kafka directly

Design guidelines

General

  • shared code items:
    • starfish, framework, generic response structures (responses with warnings/errors etc)
    • payloads and responses
  • phoenix must have no notion of microservice's models and tables for no "accidental" queries
  • microservice should have its own database/schema (TODO: figure the exact design here) and migrations using this approach
  • microservice URL must be configured via application.conf in phoenix
  • naming format for microservice: phoenix-$subsystem where $subsystem is something like promotions
  • technology stack may differ from the one used in phoenix
    • we want to replace json4s with circe
    • we want to replace akka-http with something else (TODO: make decision)
    • otherwise, depending on requirements and circumstances, we can decide to change the stack, but this decision must be carefully discussed
  • project dependencies and versions must be defined in phoenix project/ and reused by microservices

APIs, payloads and responses

  • payloads and responses serve as an API contract between phoenix and microservices
  • if payload or response is changed on one side (e.g. microservice), compilation must fail at another (e.g. phoenix)
  • payloads and responses must be classified as
    • internal (phoenix ↔ microservices)
    • external (client ↔ phoenix)

Internal

Internal API can assume that a "communication context" exists, and does not need to be detailed. If a request fails, a suggested response format would be a well-formed data for phoenix to render error upon and a debug message. On the example of MWH /reservations/hold endpoint, given phoenix has sent a request to hold SKUs A, B and C: This endpoint does one thing, and irregardless of why the request can't be processed for SKUs A and B, phoenix will render "Sorry, SKUs A and B are out of stock". So phoenix only needs to know that A and B were offending. For debugging purposes, a debug section is attached to MWH response, describing that SKU A was out of stock but SKU B for some reason had no corresponding entry in database. Sample error response JSON:

{
  "errors": [
    { "sku": "A", "debug": "out of stock" },
    { "sku": "B", "debug": "no entry found in table foo_bar for foo_id=124" }
  ]
}

Logging [TBD]

For debugging purposes, communication protocol must be extended with at least requestId field to trace the request. This section is to be filled in later.

External

Phoenix returns either data or error to render on UI. Client has less context of possible failures when sending requests to phoenix, hence phoenix must provide more detail in case of failure. For now, this means rendering an error message string based on data returned from internal queries and/or microservice response. For the above example of MWH hold failure, phoenix must collect offending SKU codes from MWH response and render a string like "Impossible to complete checkout because some items are out of stock. Please remove SKUs A and B to proceed". Better phoenix error API is currently under development.

Refactoring execution order

  1. API contract design
  2. microservice implementation
  3. microservice testing (preferably including performance testing)
  4. phoenix refactoring to use the new microservice
    • in case of rewriting microservice, change in config to point to new impl

Microservices

Promotions

Promotion is "composed" of qualifier and offer. We need to check is some cart metadata qualifies against any promotions, and if so, compare promotions' offers against current total and return the best one.

Copying things over from phoenix impl, here's the current spec:

Qualifiers:

  • any (no checks)
  • number of items
  • total cost of items
  • total cost of cart
  • customer dynamic group

Offers:

  • percent off one item
  • percent off multiple items
  • amount off one item
  • amount off multiple items
  • free shipping

Basic endpoints

Fetch, create, update and archive endpoints must be carried over from phoenix as is.

Auto-apply

POST /v1/promotions/auto-apply

Returns 200 OK and the best (that will shave off the biggest sum off cart total) auto-apply promotion for given cart metadata.

Returns 204 NoContent if there isn't an applicable auto-apply promo.

phoenix → phoenix-promotions

Cart metadata

customerGroupIds: Option[NEL[Int]] // for DCG-based promos
appliedCouponId(s): Option[NEL[Int]] // for (non-)exclusive promos
cartTotal: Int // does not equal to sum of line item totals, also includes shipping cost and possibly some other unanticipated stuff
shippingCost: Int // to estimate if free shipping offer is beneficial
lineItems: NEL[
  skuId: Int // for offers based on items
  type: Regular | GiftCard // promo can omit GCs in qualification
  quantity: Int // for offers based on item qty
  unitPrice: Int
]

Request should contain all data for phoenix-promotions to run all qualifier checks and decide on promotion. Putting this note in case description is missing something.

To support the idea of API acting as spec and contract: looking at this payload, one can assume that all the data passed there makes sense for qualifier judgement.

phoenix-promotions → phoenix

Promotion details

id: Int
name: String
customAttrs: Json // maybe
offer: PercentOff(skuIds: [Int], percentOff: Int) | AmountOff(skuIds: [Int], amountOff: Int) | FreeShipping

Coupons

TBD