How Bluetext makes building world-class enterprise-grade distributed systems easy
In Bluetext, a system is declared in one centralized place with a homogenized expression. The declaration specifies which runtime-separated parts the system is composed of, how those parts should be run in different environments, how they should be deployed and how they should communicate with each other. That makes it easy to get an overview of the system, and to follow established enterprise software best practices in order to meet the toughest requirements for compliance, security, reliability, maintainability, scalability, performance, flexibility, cost-effectiveness and development velocity.
Normally this information is spread across a compose file for development, charts for the cluster, a gateway's own configuration, an API description per contract, and an application config file per service — each a different language, owned by a different tool, and none of them knowing the others exist.
Here is an overview of the most significant Bluetext constructs:
How the system's runtime-separated parts are managed
| Construct | What it settles | Easy now, hard before |
|---|---|---|
| Components | What the system is made of — the api, the gateway, the identity server, the store — each declaring what it offers and how others use it | Add a part to the system by declaring it, and see the whole system in one list, checked for consistency before anything is built. Before, the list was implicit: a compose file, a set of charts, a CI pipeline and a README, with no single answer to what the system consisted of and nothing checking that the answers agreed |
| Component kinds | Which of three things a component is: a tool people install on their own machine, a service other things call over the network, or a web page's files served to a browser | Put a CLI tool, a network service and a web page in one system and have each delivered the way it needs to be — a published binary, a running workload, files behind a CDN — without maintaining three release processes. Before, each of those was its own pipeline with its own tooling and conventions, and a release touching all three was coordinated by hand |
| Variants | The several ways one component can be made real, with the environment choosing which | Run the database as a container in development and a managed service in production, and nothing that uses it changes. Before, you either pointed development at the managed service itself — shared state, rate limits, no offline work, no clean reset — or kept a second configuration for a local stand-in that drifted from it |
| Variant kinds | Who builds a component and who runs it: we do both, we run somebody else's build, or somebody else does both | Mix your own binaries, vendors' images and services you do not run at all in one system, and have the deploy know which is which. Before, a hosted dependency was usually not described anywhere — a URL in a config file with nowhere to record its address or its credentials — so nothing said where your responsibility ended |
How the parts connect
| Construct | What it settles | Easy now, hard before |
|---|---|---|
| Links | What this component calls, declared by the component doing the calling | Call another component without knowing its address or holding its password — declare the link and the client, the address and the password all arrive with it. Before, this was assembled by hand for each environment: an environment variable for the host, a mounted file for the password, a client library pinned by whoever integrated it first — and each of the three could be wrong on its own |
| Interfaces | What a component offers to others, and what a caller has to present to use it | Change a contract and every caller's code changes with it. Before, generated clients were already normal, but the written contract and the running code could disagree — nothing forced a handler to match the document its client was generated from |
| Profiles | Each distinct way of using one interface, and the credential that particular use presents | Let two callers use the same component at different levels of access, without either one borrowing the other's credential. Before, one account was shared by everything that talked to a component, because splitting it meant duplicating the whole connection setup — so least privilege lost to convenience |
| Routes | Which components this one's gateway forwards traffic to, as opposed to what its own code calls | Put a page, an API and a login flow behind a single address, with the gateway's whole configuration generated from the declaration. Before, that configuration was a file kept by hand, repeating every backend's address and path and drifting from what it actually fronted — and a service also published directly, beside its own gateway route, was a second way in that nothing downstream could detect |
| Credential bundles | Which credential each connection presents, and which keys make that credential up | Know every secret the system uses, and rotate one without first hunting for where it was copied. Before, credentials were spread across chart values, sealed secrets and mounted files, with no single list of what existed or who presented what |
How the code is structured
| Construct | What it settles | Easy now, hard before |
|---|---|---|
| Endpoints | The operations an interface exposes: the method, the path, what goes in and comes back, and the errors it is allowed to return | Declare an operation and get its handler, the client other code calls it with, and its error type from one place — with no way to leave a failure unhandled. Before, the failure cases were whatever each handler happened to return, a caller's error handling was written from documentation rather than from the contract, and the mismatch showed up in production |
| Gates | Where permission is decided: one gate per operation, deciding whether this caller is allowed to do this, with the work itself in a command beside it | Review a system's permissions by reading its gates, and add an operation without being able to expose it by accident. Before, proving that every way in checked permission meant auditing every route and every handler, and a route added without its check was an ordinary kind of mistake |
| The model | Where the business logic lives | Find every business decision in one place, and know that no part of the code can reach data that is not its own. Before, keeping logic in one layer was a convention, so it accumulated in controllers and clients wherever that was convenient, and nothing prevented it |
| State variables | Which data the system itself owns, as against data it only reads from somebody else's service | Answer what data the system holds and where it lives from the code that holds it, and see from any piece of code which service its reads and writes went to. Before, the data inventory was a document maintained by hand, so it went stale the moment the code changed, and every audit began by rediscovering the truth |
| Drivers | The client a component talks to one dependency through, carrying that product's own API rather than a simplified version of it | Use a product's real API — its own operation names and shapes — without writing or wiring a client, and adopt a new datastore without waiting for a new release of the platform. Before, a dependency was either wrapped in a thin abstraction that hid most of what it could do, or given a client maintained by hand for each service and each language |
How the system is deployed
| Construct | What it settles | Easy now, hard before |
|---|---|---|
| Targets | Where a deploy lands: a cluster on a laptop, a managed cluster in a cloud, a CDN | Land the same system on a laptop, a managed cluster or a CDN, with each provider's differences written down in one place. Before, portability was claimed by every layer and delivered by none — the manifests moved and the ingress, storage classes and registries did not. Preparing a cluster is still real work, but it is a short list stated up front rather than a fault diagnosed per component (§9) |
| Deploy configs | Which environment uses which targets, and whether a developer's own running code stands in for one of the workloads | Exercise production's own router, ingress, TLS and login flow from a laptop. Before, a local stack ran a different router and neither of the other two, so the failures that survived into production were precisely the ones development never exercised (§7) |
| Value interpolation | The handful of values that genuinely differ between environments, written once and filled in wherever they are used | Change an environment by editing the values that differ, not every file that mentions them. Before, the same hostname was written into a chart, a config template and application code, and moving an environment meant finding all three and hoping you had |
| Secrets | Where secret material is kept, and what is still missing before the system can run | Set a secret once and have everything that needs it receive it at deploy time — and find out what is still missing before deploying rather than after. Before, material was copied by hand into a chart value, a CI variable and a developer's env file, and a missing one announced itself as a crash after rollout |
| Secrets projection | How addresses and credentials reach a running process: as files on disk beside it, one set per connection | Nothing is in an environment variable and nothing is in the image. Before, credentials arrived as environment variables that leaked into logs, crash dumps and process listings, or were built into an image that had to be rebuilt to rotate one |
| file-config | A bought-in product's own configuration file, in its own format, at the path where it looks for it | Let a vendor's product read its own config file, generated from the declaration with this environment's addresses and this deploy's credentials already in it. Before, a vendor config was a file checked in per environment, forked and drifting, with credentials either committed or patched in by a deploy script |
| api-config | Setup a product only accepts through its own API, which has to be in place before anything that uses it starts | Have a bucket, a schema or a client registration exist before the first consumer runs, applied through the product's own API as part of the deploy. Before, that was a one-off script someone remembered to run, or an init container racing the workload — and whoever started first failed in a way that looked like their own bug |
Each of those is settled by the platform rather than by the team — the best practices of enterprise-grade development and maintenance, carried by the constructs instead of learned by being burned.