Design patterns
The patterns the SDK is built from: one resource layout, fluent builders and updaters, model objects that carry their own operations, adapters, a declarative endpoint catalogue and a gateway for the design layer.
One layout for every resource#
Every API resource in every domain is built from the same parts. A developer who has used one resource can use any other without reading its documentation first, because the verbs, the builder shape and the returned objects are the same throughout.
| Part | Example | Role |
|---|---|---|
| Client interface | Connections | The public entry point: list, search, getByUuid, define, update |
| Operator | ConnectionOperator | Holds the builder and the updater of the resource |
| Builder | ConnectionOperator.ConnectionBuilder | Collects typed arguments, then create() |
| Updater | ConnectionOperator.ConnectionUpdater | Collects JSON Patch operations, then save() |
| Creator JSON | ConnectionCreatorJson | The request body, serialised by Jackson |
| Response JSON | ConnectionJson | The response body, deserialised by Jackson |
| Wrapper | ConnectionWrapper | Implements the model interface by delegating to the response JSON, and adds the operations |
| Model interface | Connection | What the caller holds: getters, update(), delete(), refresh() |
A read-only resource uses the same layout without the operator and the wrapper.
Builders: define, then create#
A create is one fluent expression. define(...) returns the builder, each step sets one typed part of the request, and create() sends it. The builder takes enums wherever the specification defines an enumeration, and it takes model objects or UUID strings wherever one resource refers to another, so an invalid request is hard to write.
import com.eqixiac.equinix.fabric.enums.ConnectionType;
import com.eqixiac.equinix.fabric.model.Connection;
import com.eqixiac.equinix.fabric.model.implementation.LinkProtocol;
import com.eqixiac.equinix.fabric.model.implementation.cloud.AwsDirectConnectAdapter;
Connection connection = fabric.connections()
.define(ConnectionType.EVPL_VC)
.name("dc-port-to-aws")
.bandwidth(100)
.aSideAccessPointPort(portUuid, LinkProtocol.dot1q().vlanTag(1000).create())
.zSideCloudProvider(AwsDirectConnectAdapter.of("123456789012", "us-east-1", awsProfileUuid, "DC"))
.notification("noc@example.com")
.create();The builder is an inner class of the operator, so it has direct access to the client that will send the request and needs no reference passed around. Nothing is sent until create().
One switch for dry run#
Validation is the same expression with one more step. dryRun() sets a flag on the builder, and the request goes to the same endpoint with dryRun=true. There is no second API to learn and no second code path to keep in step with the first.
Connection validated = fabric.connections()
.define(ConnectionType.EVPL_VC)
.name("dc-port-to-aws")
.bandwidth(100)
.aSideAccessPointPort(portUuid, LinkProtocol.dot1q().vlanTag(1000).create())
.zSideCloudProvider(AwsDirectConnectAdapter.of("123456789012", "us-east-1", awsProfileUuid, "DC"))
.notification("noc@example.com")
.dryRun()
.create();Updaters: the patch writes itself#
An update is also one fluent expression. Each step on the updater appends one RFC 6902 operation with the correct path, and save() sends the document. The caller never writes a JSON pointer.
Connection existing = fabric.connections().getByUuid(connectionUuid);
existing.update()
.name("dc-port-to-aws-prod")
.bandwidth(500)
.save();
existing.update().bandwidth(1000).dryRun().save(); // validate the change onlyModel objects that carry their operations#
A model object knows the client it came from. connection.update(), connection.delete() and connection.refresh() act on the resource the object represents, so code reads in terms of the resource and not in terms of UUIDs passed back to a client.
Connection live = fabric.connections().getByUuid(connectionUuid);
live.refresh(); // read the current state again
System.out.println(live.getName() + " " + live.getState());
live.delete();
System.out.println(live.isDeleted()); // the object keeps its data after the deleteThe wrapper achieves this by delegation. It implements the model interface, forwards every getter to the response JSON object, and adds the operations. The caller's type is the interface, so the JSON classes can change with the API without changing caller code.
Value objects with small factories#
The parts of a request that have their own structure are built by small factories with the same create() ending: LinkProtocol.dot1q().vlanTag(1000).create(), LinkProtocol.qinq(), LinkProtocol.untagged(), SimpleAccessPoint.define(...). Each one is usable wherever the builder accepts that part.
Adapters for cloud providers#
A cloud connection needs the provider's identifiers in the form Fabric expects. CloudProviderConnectionAdapter<T> is the adapter interface, and AwsDirectConnectAdapter, AzureExpressRouteAdapter, GoogleCloudInterconnectAdapter and OracleFastConnectAdapter implement it. The connection builder accepts any adapter through zSideCloudProvider(adapter), so a provider without a built-in adapter is supported by implementing one interface (Adapter for another provider).
Endpoints as data#
URLs, verbs and query parameters are not written in Java. Each functional area has a JSON catalogue, apiParams_*.json, that declares its endpoints, and the internal client assembles requests from it. Adding an operation is an entry in the catalogue plus the typed method that calls it, and a change of path in a new API version is a change to one line of data.
One transport for every domain#
Authentication, retries, 401 recovery, pagination, error mapping and correlation identifiers are implemented once in the core client. Every domain client inherits them, which is why pagination, retries and the error model are described once and apply everywhere.
Tolerant reading#
The read side is written so that the API can grow without breaking callers. Unknown JSON properties are ignored, an enum value the SDK does not list reads as UNKNOWN, and where the live service and the specification disagree on a shape the model reads both (Forward compatibility).
Facades#
| Facade | Hides |
|---|---|
Equinix | One token and one connection pool behind every domain client |
Design | The design engines behind optimizeMetros, resilience, decisionRecord, analyzeEstate, deploymentWizard |
A gateway and plug-in interfaces for the design layer#
The design engines do not depend on the Fabric class. They depend on FabricGateway, an interface with the six resource clients they use (metros, service profiles, Cloud Routers, connections, routing protocols and prices), which Fabric implements. The engines can therefore be tested against a stub gateway, and they cannot reach a resource outside those six.
Inside the design layer every decision point is an interface in design.spi: Objective, Constraint, CostModel, CurrencyConverter and Solver. A DesignPlugin bundles implementations, and the built-in objectives and constraints are installed through the same interfaces as a plug-in would be (Extension points).
The inputs and results of the engines are immutable records. A DesignSpec has a builder and toBuilder() for a modified copy, and its fingerprint ties every result to the exact input that produced it.