Blog

Luis Majano

April 09, 2026

Spread the word


Share your thoughts

We just shipped the BoxLang Google Cloud Functions Runtime β€” and it brings the same write-once-run-anywhere serverless experience you already know from our AWS Lambda runtime, now running natively on Google Cloud Functions Gen2.

The big idea is simple: the same .bx handler file you deploy to AWS Lambda works on GCF without modification. One codebase. Two clouds. Zero rewrites.

The Architecture

The GCF experience is intentionally split into two projects:

Your business logic lives entirely in BoxLang files under src/main/bx. Java is only the serverless bridge β€” you never touch it.

The runtime entry point is ortus.boxlang.runtime.gcp.FunctionRunner, which implements Google Cloud's HttpFunction interface. On cold start the BoxLang runtime initializes once. Compiled .bx classes are cached internally and reused across all warm invocations β€” you pay the compilation cost exactly once.

Documentation

Of course, everything is fully documented: https://boxlang.ortusbooks.com/getting-started/running-boxlang/google-cloud-functions

FIU Collaboration

This runtime was built in collaboration with Florida International University (FIU) and a talented team of students who contributed to making BoxLang on Google Cloud Functions a reality. We are incredibly grateful for their hard work and dedication to the BoxLang ecosystem. πŸŽ“


Writing Handlers

Handlers are plain BoxLang classes that expose a run() method:

class {

    function run( event, context, response ) {
        response.statusCode = 200
        response.body = {
            "error"    : false,
            "messages" : [],
            "data"     : "Hello from BoxLang on GCF!"
        }
    }

}

Every handler method receives three arguments:

ArgumentTypeDescription
eventStructAll incoming request data β€” method, path, headers, body, query
contextStructGCF metadata β€” function name, revision, project ID, request ID
responseStructMutable response struct β€” statusCode, headers, body, cookies

Returning a struct or array from your method JSON-serializes it automatically. Returning a plain string writes it verbatim. Or set response.body directly for explicit control.

A single handler class can expose multiple methods:

class {

    function run( event, context, response ) {
        response.statusCode = 200
        response.body = { "resource": "customers", "path": event.path }
    }

    function findById( event, context, response ) {
        response.statusCode = 200
        response.body = { "action": "findById", "path": event.path }
    }

}

Two Layers of Routing

The runtime gives you two independent dispatch layers, which together let you build clean multi-resource APIs from a single function deployment.

Layer 1 β€” URI β†’ Handler Class

The first URI path segment is converted to PascalCase and matched against .bx files in your handler root. If no match is found, Lambda.bx is the fallback:

Request URIResolved Handler
/Lambda.bx
/customersCustomers.bx
/customers/123Customers.bx
/productsProducts.bx
/user-profilesUserProfiles.bx
/unknownLambda.bx

To add a route, just drop a PascalCase .bx file in src/main/bx. No config. No routing tables. Convention does the work.

src/main/bx/
  Application.bx
  Lambda.bx         ← fallback + root route
  Customers.bx      ← handles /customers/**
  Products.bx       ← handles /products/**
  UserProfiles.bx   ← handles /user-profiles/**

Layer 2 β€” x-bx-function β†’ Method

Once the handler class is resolved, you can route to any method in it using the x-bx-function request header. Without the header, run() is called by default:

# Calls Lambda.bx::run()
curl http://localhost:9099/

# Calls Lambda.bx::anotherLambda()
curl -H "x-bx-function: anotherLambda" http://localhost:9099/

# Calls Customers.bx::findById()
curl -H "x-bx-function: findById" http://localhost:9099/customers/123

URI path selects the class. x-bx-function selects the method. Two clean axes of dispatch.


The Event Struct β€” Portable By Design 🎯

This is the part worth paying attention to. The event struct your handler receives mirrors the AWS API Gateway v2.0 HTTP event format β€” intentionally:

{
    method                : "GET",
    path                  : "/products/42",
    rawPath               : "/products/42",
    headers               : { "content-type": "application/json" },
    queryStringParameters : { page: "1" },
    body                  : "",
    requestContext        : {
        http : {
            method : "GET",
            path   : "/products/42"
        }
    }
}

If you're already running BoxLang on AWS Lambda, your handlers drop into GCF with zero changes. Same struct shape. Same handler signature. Same return conventions.


Run Locally in Seconds

Clone the starter and you're running in three commands:

git clone https://github.com/ortus-boxlang/boxlang-starter-google-functions.git
cd boxlang-starter-google-functions
./gradlew clean test
./gradlew runFunction

Expected output:

================================================================
 BoxLang GCF Function Invoker
 Listening on  : http://localhost:9099
 Function root : .../src/main/bx
 Debug mode    : true
 Press Ctrl+C to stop.
================================================================

The runFunction task uses the official Google Functions Java Invoker under the hood and wires it to FunctionRunner automatically. Local overrides are available as Gradle flags:

./gradlew runFunction -PtestPort=8080       # custom port
./gradlew runFunction -PdebugMode=true      # verbose logging + live reload
./gradlew runFunction -PfunctionRoot=/path  # custom handler directory

Enable debugMode and .bx file changes are picked up live β€” no JAR rebuild, no restart. Never enable debug mode in production β€” it disables class caching.

Smoke Testing Locally

# Default handler
curl http://localhost:9099/

# POST with JSON body
curl -X POST http://localhost:9099/ \
  -H "Content-Type: application/json" \
  -d @workbench/sampleRequests/event-local.json

# Method routing
curl -H "x-bx-function: anotherLambda" http://localhost:9099/

Testing

The starter ships with a full integration test suite using JUnit and Google Truth. Tests run against real GCF mock HTTP objects β€” no mocking frameworks:

Test ClassCoverage
FunctionRunnerTestRequest lifecycle, URI routing, method routing, JSON responses, concurrency
MockHttpRequestFluent test request builder
MockHttpResponseCaptures status, body, and headers
./gradlew test

# Run just the integration suite
./gradlew test --tests "com.myproject.FunctionRunnerTest"

# Open the HTML report
open build/reports/tests/test/index.html

Build and Deploy

Build the Deployable Package

./gradlew clean shadowJar buildLambdaZip

This produces a deployable ZIP at:

build/distributions/boxlang-google-function-project-<version>.zip

The ZIP contains everything GCF needs β€” .bx handlers at the package root, boxlang.json, boxlang_modules/, and the runtime JAR with all dependencies in lib/.

Deploy to Google Cloud Functions Gen2

gcloud auth login
gcloud config set project YOUR_PROJECT_ID

gcloud functions deploy YOUR_FUNCTION_NAME \
  --gen2 \
  --runtime=java21 \
  --region=us-central1 \
  --entry-point=ortus.boxlang.runtime.gcp.FunctionRunner \
  --trigger-http \
  --allow-unauthenticated \
  --source=build/distributions/boxlang-google-function-project-1.0.0.zip

If you changed version in gradle.properties, update the ZIP filename in --source accordingly.


Environment Variables

VariableDescription
BOXLANG_GCP_ROOTRoot directory for .bx handlers
BOXLANG_GCP_CLASSOverride default handler path
BOXLANG_GCP_DEBUGMODEEnable verbose logging and disable class caching
BOXLANG_GCP_CONFIGCustom boxlang.json path
K_SERVICEFunction name (set automatically by GCF Gen2)
K_REVISIONFunction revision (set automatically by GCF Gen2)
GOOGLE_CLOUD_PROJECTProject ID (set automatically by GCF Gen2)

BoxLang Modules Work Too

Any BoxLang module works with this runtime. Include it in your deployment ZIP under boxlang_modules/ and point BOXLANG_GCP_CONFIG at your boxlang.json. The entire module ecosystem β€” database, caching, AI, and more β€” is available inside your serverless functions.


Get Started

πŸš€ Starter Template: github.com/ortus-boxlang/boxlang-starter-google-functions

πŸ“¦ Runtime Source: github.com/ortus-boxlang/boxlang-google-functions

πŸ“š BoxLang Docs: boxlang.ortusbooks.com


BoxLang is built to run everywhere β€” web servers, AWS Lambda, Google Cloud Functions, WebAssembly, mobile, and more. Every new runtime we ship gets you closer to a world where your application logic outlives any single cloud vendor's pricing model.

Write once. Run anywhere. Ship faster. ⚑

Add Your Comment

Recent Entries

BoxLang v1.12.0 - Destructuring, Spread, Ranges, Watchers, Oh My!

BoxLang v1.12.0 - Destructuring, Spread, Ranges, Watchers, Oh My!

BoxLang 1.12.0 marks a meaningful turning point. After establishing a rock-solid foundation across runtime, compiler, CFML compatibility, and the module ecosystem, BoxLang has entered its innovation cycle. The language is mature, battle-tested, and production-deployed across the industry.

Luis Majano
Luis Majano
April 08, 2026
The Loneliness of CTO Leadership: How to Make Important Decisions with Confidence

The Loneliness of CTO Leadership: How to Make Important Decisions with Confidence

Being a CTO can be surprisingly isolated.

Not because of the title, but because of the decisions.

Every day, you’re expected to make calls that impact system stability, performance, security, team productivity, and long term costs. And often, you’re making those decisions with incomplete information, limited resources, and no real sounding board.

That’s where the real challenge begins.


It’s Not a Talent Problem, It’s a Context Problem

Most CTOs don’...

Cristobal Escobar
Cristobal Escobar
April 07, 2026