IMR Lopez Logo

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.

schema.ts
rules.ts
commands.ts
queries.ts
triggers.ts

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:

  1. Language Independence: Generate type-safe clients for any language
  2. Protocol Flexibility: Switch between gRPC, HTTP, TCP without changing business logic
  3. Backward Compatibility: Contracts enforce API stability
  4. Documentation: Contracts serve as living documentation
proto/order/v1/order.proto
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:

domain/catalog/commands.ts
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

ProtocolUse CaseLatencyFeatures
gRPCService-to-serviceVery LowStreaming, binary, HTTP/2
HTTPS/RESTPublic APIs, browsersLowUniversal, cacheable
TCPHigh-frequency tradingUltra LowRaw sockets, custom framing
WebSocketReal-time updatesLowBi-directional, persistent
Message QueueAsync processingVariableDurability, 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

.changeset/config.json
{
  "$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
  }
}

Mentions

On this page