How to Build a Flexible SaaS Ordering System with Serverless Workflows and Functions
This article explains how SaaS providers can overcome rigidity by using Alibaba Cloud Serverless Workflow and Function Compute to orchestrate a customizable order‑processing module, detailing the architecture, FDL definitions, Java SDK integration, Vue front‑end, and payment routing logic.
As the internet’s population dividend wanes, growth based on traffic is slowing, prompting the industry to seek new blue‑sea opportunities. The rise of the industrial internet—where cloud computing, big data, and AI are embedded into traditional sectors—has highlighted SaaS as a crucial B2B component, encompassing CRM, HRM, expense control, finance, and collaboration tools.
In the industrial‑internet era, SaaS faces unprecedented challenges: systems must be highly flexible and extensible to meet diverse, custom‑made requirements from enterprise customers, rather than relying on mass traffic. Companies can no longer grow solely by burning cash to acquire users; they must enable deep product customization.
While building a full aPaaS platform is costly, a lightweight and effective alternative is to adopt Serverless solutions. Serverless Workflow, a fully managed cloud service, can coordinate distributed tasks (functions, APIs, VM programs) using sequential, branching, or parallel flows, providing reliable execution, retry logic, logging, and audit capabilities.
The core idea is to reconstruct SaaS flexibility by extracting the most frequently customized features, exposing them as independent functions, and orchestrating those functions with Serverless Workflow. The example below demonstrates a complete order‑processing module built with Java, Vue, Serverless Function Compute (FC), and Serverless Workflow.
1. Defining the workflow with FDL
The workflow is described using the Flow Definition Language (FDL). The following snippet shows the full definition, including task nodes, a choice node, and end nodes:
version: v1beta1
type: flow
timeoutSeconds: 3600
steps:
- type: task
name: generateInfo
timeoutSeconds: 300
resourceArn: acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
pattern: waitForCallback
inputMappings:
- target: taskToken
source: $context.task.token
- target: products
source: $input.products
- target: supplier
source: $input.supplier
- target: address
source: $input.address
- target: orderNum
source: $input.orderNum
- target: type
source: $context.step.name
outputMappings:
- target: paymentcombination
source: $local.paymentcombination
- target: orderNum
source: $local.orderNum
serviceParams:
MessageBody: $
Priority: 1
catch:
- errors:
- FnF.TaskTimeout
goto: orderCanceled
- type: task
name: payment
timeoutSeconds: 300
resourceArn: acs:mns:::/topics/payment-fnf-demo-jiyuan/messages
pattern: waitForCallback
inputMappings:
- target: taskToken
source: $context.task.token
- target: orderNum
source: $local.orderNum
- target: paymentcombination
source: $local.paymentcombination
- target: type
source: $context.step.name
outputMappings:
- target: paymentMethod
source: $local.paymentMethod
- target: orderNum
source: $local.orderNum
- target: price
source: $local.price
- target: taskToken
source: $input.taskToken
serviceParams:
MessageBody: $
Priority: 1
catch:
- errors:
- FnF.TaskTimeout
goto: orderCanceled
- type: choice
name: paymentCombination
inputMappings:
- target: orderNum
source: $local.orderNum
- target: paymentMethod
source: $local.paymentMethod
- target: price
source: $local.price
- target: taskToken
source: $local.taskToken
choices:
- condition: $.paymentMethod == "zhifubao"
steps:
- type: task
name: zhifubao
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
inputMappings:
- target: price
source: $input.price
- target: orderNum
source: $input.orderNum
- target: paymentMethod
source: $input.paymentMethod
- target: taskToken
source: $input.taskToken
- condition: $.paymentMethod == "weixin"
steps:
- type: task
name: weixin
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/weixin-fnf-demo
inputMappings:
- target: price
source: $input.price
- target: orderNum
source: $input.orderNum
- target: paymentMethod
source: $input.paymentMethod
- target: taskToken
source: $input.taskToken
- condition: $.paymentMethod == "unionpay"
steps:
- type: task
name: unionpay
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/union-fnf-demo
inputMappings:
- target: price
source: $input.price
- target: orderNum
source: $input.orderNum
- target: paymentMethod
source: $input.paymentMethod
- target: taskToken
source: $input.taskToken
default:
goto: orderCanceled
- type: task
name: orderCompleted
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/orderCompleted
end: true
- type: task
name: orderCanceled
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/cancerOrderThe workflow coordinates the order lifecycle: generateInfo → payment → paymentCombination (routing to a specific payment method) → orderCompleted or orderCanceled.
2. generateInfo node
This task sends a message to an MNS topic (generateInfo‑fnf‑demo‑jiyuan) and waits for a callback. Input mappings pass the task token, product list, supplier, address, and order number. Output mappings return the chosen payment combination and order number.
3. generateInfo‑fnf‑demo function (Python)
# -*- coding: utf-8 -*-
import logging, json, requests
from aliyunsdkcore.client import AcsClient
from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest, ReportTaskFailedRequest
def handler(event, context):
region = "cn-hangzhou"
account_id = "XXXX"
ak_id = "XXX"
ak_secret = "XXX"
fnf_client = AcsClient(ak_id, ak_secret, region)
logger = logging.getLogger()
body = json.loads(event)
supplier = body["supplier"]
taskToken = body["taskToken"]
orderNum = body["orderNum"]
# Simple rule: different suppliers support different payment combos
if supplier == "haidilao":
paymentcombination = "zhifubao,weixin"
else:
paymentcombination = "zhifubao,weixin,unionpay"
# Update order via a Java‑exposed API (demo URL)
url = f"http://xx.xx.xx.xx:8080/setPaymentCombination/{orderNum}/{paymentcombination}/0"
requests.get(url)
output = f"{{\"orderNum\": \"{orderNum}\", \"paymentcombination\": \"{paymentcombination}\"}}"
req = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
req.set_Output(output)
req.set_TaskToken(taskToken)
fnf_client.do_action_with_exception(req)
return 'hello world'4. Starting the workflow from Java
Add the following Maven dependencies:
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>[4.3.2,5.0.0)</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-fnf</artifactId>
<version>[1.0.0,5.0.0)</version>
</dependency>Configure the SDK client:
@Configuration
public class FNFConfig {
@Bean
public IAcsClient createDefaultAcsClient(){
DefaultProfile profile = DefaultProfile.getProfile(
"cn-xxx", // region
"ak", // AccessKey ID
"sk"); // AccessKey Secret
return new DefaultAcsClient(profile);
}
}Expose a GET endpoint to start the flow:
@GetMapping("/startFNF/{fnfname}/{execuname}/{input}")
public StartExecutionResponse startFNF(@PathVariable("fnfname") String fnfName,
@PathVariable("execuname") String execuName,
@PathVariable("input") String inputStr) throws ClientException {
JSONObject json = new JSONObject();
json.put("fnfname", fnfName);
json.put("execuname", execuName);
json.put("input", inputStr);
return fnfService.startFNF(json);
}The service creates a StartExecutionRequest, sets the flow name, execution name (order number), and input JSON, stores a temporary Order object in a map, and calls getAcsResponse to launch the workflow.
5. Vue front‑end for order creation
When the user selects a product, supplier, and address, the front‑end calls the Java start‑flow API:
submitOrder(){
const orderNum = uuid.v1();
this.$axios.get(`/startFNF/OrderDemo-Jiyuan/${orderNum}/{
"products": "${this.products}",
"supplier": "${this.supplier}",
"orderNum": "${orderNum}",
"address": "${this.address}"
}`).then(response => {
if(response.message == "success"){
this.$router.push(`/orderdemo/${orderNum}`);
}
})
}6. payment node
After the user reaches the payment page, the workflow sends a message to the MNS topic payment‑fnf‑demo‑jiyuan, triggering the payment‑fnf‑demo function.
7. payment‑fnf‑demo function (Python)
# -*- coding: utf-8 -*-
import logging, json, time, requests
from aliyunsdkcore.client import AcsClient
from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
from mns.account import Account
def handler(event, context):
logger = logging.getLogger()
region = "cn-xxx"
ak_id = "xxx"
ak_secret = "xxx"
mns_endpoint = "http://your_account_id.mns.cn-hangzhou.aliyuncs.com/"
queue_name = "payment-queue-fnf-demo"
account = Account(mns_endpoint, ak_id, ak_secret)
queue = account.get_queue(queue_name)
fnf_client = AcsClient(ak_id, ak_secret, region)
eventJson = json.loads(event)
while True:
try:
msg = queue.receive_message(30)
queue.delete_message(msg.receipt_handle)
msgJson = json.loads(msg.message_body)
task_token = eventJson["taskToken"]
orderNum = eventJson["orderNum"]
output = f"{{\"orderNum\": \"{orderNum}\", \"paymentMethod\": \"{msgJson[\"paymentMethod\"]}\", \"price\": \"{msgJson[\"price\"]}\"}}"
req = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
req.set_Output(output)
req.set_TaskToken(task_token)
fnf_client.do_action_with_exception(req)
break
except Exception as e:
logger.info("retry waiting for payment message")
return 'hello world'This function polls the MNS queue until a payment‑method message arrives, then reports success back to the workflow.
8. paymentCombination choice node
Based on the paymentMethod returned by the previous step, the workflow routes to the corresponding payment‑specific task (zhifubao, weixin, or unionpay). The default branch leads to orderCanceled.
9. Payment‑method functions (zhifubao, weixin, unionpay)
Each function receives price, orderNum, and paymentMethod, applies a discount, updates the order via the Java API, and returns ok. Example for zhifubao:
# -*- coding: utf-8 -*-
import logging, json, requests
from aliyunsdkcore.client import AcsClient
from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
def handler(event, context):
body = json.loads(event)
price = int(body["price"])
new_price = int(price * 0.8) # 20% discount for zhifubao
orderNum = body["orderNum"]
paymentMethod = body["paymentMethod"]
url = f"http://xx.xx.xx.xx:8080/setPaymentCombination/{orderNum}/{paymentMethod}/{new_price}"
requests.get(url)
return {"Status": "ok"}Weixin applies a 50% discount, unionpay a 30% discount; the logic can be customized as needed.
10. Complete flow
The final workflow includes placeholder orderCompleted and orderCanceled nodes where developers can add business‑specific actions. The overall execution path is:
11. Takeaways
Define merchant‑payment metadata so that the workflow can retrieve supported payment methods dynamically.
Expose the payment‑selection page as a Vue component that queries the order API and displays the appropriate options.
To add a new payment channel, simply extend the paymentCombination choice node with a new branch and implement the corresponding function.
This example illustrates how Serverless Workflow and Function Compute can be combined to give SaaS products the flexibility and extensibility required in the industrial‑internet era.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Alibaba Cloud Native
We publish cloud-native tech news, curate in-depth content, host regular events and live streams, and share Alibaba product and user case studies. Join us to explore and share the cloud-native insights you need.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
