Infrastructure as Code
Modular monorepo architecture combining microservices, CQRS, event sourcing, and serverless patterns with shared SDKs and orchestrated CI/CD pipelines.
Infrastructure as Code
Infrastructure as Code (IaC) Monorepo Architecture combines all architectural patterns (microservices, CQRS, event sourcing, serverless, hexagonal) into a unified, modular codebase. Services share common SDKs published as npm packages, with CI/CD pipelines that orchestrate deployments across the entire organization.
This architecture enables teams to deploy services independently while sharing core business logic through internal SDKs, ensuring consistency and reducing duplication across the organization.
The shape that matters is the direction of dependencies. Transport knows about the domain; the domain never knows about transport.
The single rule that keeps this honest: the domain must not import the transport. If commands.ts imports an HTTP type, the layering is already gone — you just can't see it yet.
That direction is what makes a module liftable. A domain folder with its own schema, rules, and commands can be extracted into a separate service without archaeology, because nothing inside it knows how it was called.
Why This Architecture?
SDK Modules
SDKs are the glue between your services and the platform. They provide type-safe interfaces that abstract away communication protocols, allowing services written in any language to interact seamlessly.
The SDK defines contracts (types, schemas, protocols), not implementations. A TypeScript service, Go service, or .NET service can all use their respective SDK to communicate using the same underlying protocol (gRPC, HTTP, TCP, WebSocket).
Architecture Overview
Core Principle: Contract-First Development
The SDK defines the contract, services implement the contract. This separation enables:
- Language Independence: Generate type-safe clients for any language
- Protocol Flexibility: Switch between gRPC, HTTP, TCP without changing business logic
- Backward Compatibility: Contracts enforce API stability
- Documentation: Contracts serve as living documentation
syntax = "proto3";
package org.order.v1;
option go_package = "github.com/org/sdk-go/order/v1";
option csharp_namespace = "Org.Order.V1";
// Order service definition
service OrderService {
// Create a new order
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
// Get order by ID
rpc GetOrder(GetOrderRequest) returns (Order);
// Stream order updates
rpc WatchOrder(WatchOrderRequest) returns (stream OrderEvent);
// List orders with filtering
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
}
message CreateOrderRequest {
string customer_id = 1;
repeated OrderItem items = 2;
Address shipping_address = 3;
}
message CreateOrderResponse {
string order_id = 1;
OrderStatus status = 2;
google.protobuf.Timestamp created_at = 3;
}
message Order {
string id = 1;
string customer_id = 2;
repeated OrderItem items = 3;
OrderStatus status = 4;
Money total = 5;
Address shipping_address = 6;
google.protobuf.Timestamp created_at = 7;
google.protobuf.Timestamp updated_at = 8;
}
message OrderItem {
string product_id = 1;
string product_name = 2;
int32 quantity = 3;
Money unit_price = 4;
}
message Money {
int64 amount = 1; // Amount in cents
string currency = 2;
}
message Address {
string street = 1;
string city = 2;
string state = 3;
string postal_code = 4;
string country = 5;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_DRAFT = 1;
ORDER_STATUS_PENDING = 2;
ORDER_STATUS_CONFIRMED = 3;
ORDER_STATUS_SHIPPED = 4;
ORDER_STATUS_DELIVERED = 5;
ORDER_STATUS_CANCELLED = 6;
}
message OrderEvent {
string order_id = 1;
OrderStatus previous_status = 2;
OrderStatus new_status = 3;
google.protobuf.Timestamp timestamp = 4;
}CQRS in practice
The pattern is easy to name and easy to fake. In a real codebase it shows up as two different files with two different shapes — writes carry an idempotency key and a permission; reads carry pagination and never mutate.
Commands are registered, not hand-written. The registry owns permission, classification and the tenant guard, so every operation inherits them:
export const catalogCommands = new TenantCommand({
defaults: {
permission: 'catalog.manage',
classification: 'restricted_mutable_master',
guard: requireCatalogTenant,
},
operations: {
'catalog.product.create': operation(
createProductCommandSchema,
products.publicDto,
'product',
),
},
})Idempotency is an argument, not a convention — the same key replays the stored result instead of writing twice.
The test for whether you really have CQRS: can you cache the read side aggressively without touching the write side? If a read path can mutate, the answer is no and the label is decoration.
Communication Protocols
| Protocol | Use Case | Latency | Features |
|---|---|---|---|
| gRPC | Service-to-service | Very Low | Streaming, binary, HTTP/2 |
| HTTPS/REST | Public APIs, browsers | Low | Universal, cacheable |
| TCP | High-frequency trading | Ultra Low | Raw sockets, custom framing |
| WebSocket | Real-time updates | Low | Bi-directional, persistent |
| Message Queue | Async processing | Variable | Durability, replay |
Versioning & Publishing
Manage synchronized releases across multiple SDK packages with automated versioning, changelogs, and publishing to package registries.
All SDKs must use the same major version to ensure protocol compatibility. Breaking changes in proto definitions require a major version bump across all SDKs.
Versioning Strategy
Changesets Configuration
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "org/platform" }
],
"commit": false,
"fixed": [
["@org/proto", "@org/sdk-typescript"]
],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [],
"privatePackages": {
"version": true,
"tag": true
},
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
}