# Design Patterns in the macro-inc/macro Codebase: A Complete Architectural Guide

> Explore design patterns in the macro-inc/macro codebase. Discover clean architecture, port-and-adapter, repository pattern, fluent builders, trait-based DI, and decorator middleware.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: architecture
- Published: 2026-08-15

---

**The macro-inc/macro codebase employs clean architecture centered on port-and-adapter (hexagonal) design, with extensive use of the repository pattern, fluent builders, trait-based dependency injection, and decorator middleware stacks.**

This article examines the specific design patterns used throughout the macro platform, a Rust-based system for enterprise document workflow automation. Understanding these patterns helps developers contribute effectively and apply similar architectural decisions to their own Rust projects.

## Port-and-Adapter (Hexagonal) Architecture

The dominant architectural style in macro is **hexagonal architecture**, also known as port-and-adapter pattern. Domain logic depends exclusively on *trait* definitions—the "ports"—while concrete implementations—the "adapters"—live in separate crates or modules.

In [`crates/webhook/src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/crates/webhook/src/domain/ports.rs), the webhook crate defines core port traits:

```rust
pub trait WebhookRepo: Clone + Send + Sync + 'static {
    fn upsert(&self, webhook: Webhook) -> Result<()>;
    async fn find_by_id(&self, id: Uuid) -> Result<Option<Webhook>>;
}

```

Adapter implementations are injected at runtime, allowing the same domain logic to operate against PostgreSQL, DynamoDB, or in-memory stores without modification. This separation is the foundation of the codebase's maintainability.

## Repository Pattern

Data access is abstracted behind traits suffixed with `Repository`. The service layer invokes these traits without awareness of underlying storage mechanisms.

The teams module demonstrates this in [`crates/teams/src/domain/team_repo.rs`](https://github.com/macro-inc/macro/blob/main/crates/teams/src/domain/team_repo.rs):

```rust
pub trait TeamRepository {
    async fn find_by_id(&self, id: TeamId) -> Result<Option<Team>>;
    async fn save(&self, team: &Team) -> Result<()>;
}

```

This pattern enables:
- **Storage swapping**: Migrate from PostgreSQL to a different database by implementing the same trait
- **Test isolation**: Provide mock repositories for unit tests
- **Clean service layer**: Business logic remains storage-agnostic

## Fluent Builder Pattern

Workflow construction helpers expose chainable APIs through the `FluentBuilder` trait. Located in [`tooling/xtask/crates/xtask_workflows/src/workflows/steps.rs`](https://github.com/macro-inc/macro/blob/main/tooling/xtask/crates/xtask_workflows/src/workflows/steps.rs), this pattern enables readable, step-by-step workflow definition:

```rust
pub trait FluentBuilder: Sized {
    fn name(self, name: impl Into<String>) -> Self;
    fn run(self, cmd: impl Into<String>) -> Self;
    fn build(self) -> Workflow;
}

impl FluentBuilder for gh_workflow::Workflow {
    // each method returns `self` for chaining
}

```

Usage follows a natural language structure: `step().with_x().and_then_y().build()`.

## Dependency Injection via Traits

Service structs receive generic parameters bounded by port traits, implementing classic **dependency injection by interface**. This appears throughout the email service in [`services/email_service/src/util/upload_attachment.rs`](https://github.com/macro-inc/macro/blob/main/services/email_service/src/util/upload_attachment.rs):

```rust
pub struct UploadAttachment<'a, S>
where
    S: SystemPropertiesService,
{
    system_properties: &'a Arc<S>,
}

impl<'a, S> UploadAttachment<'a, S>
where
    S: SystemPropertiesService,
{
    pub fn new(sys: &'a Arc<S>) -> Self { 
        Self { system_properties: sys } 
    }
    // Uses only the trait, not the concrete DB implementation
}

```

The concrete `PgSystemPropertiesRepository` and `SystemPropertiesServiceImpl` are injected through trait bounds, enabling test doubles and implementation swaps.

## Decorator (Middleware) Pattern

All HTTP servers use `tower::ServiceBuilder` to compose behavior layers. Each layer *decorates* the inner service, adding cross-cutting concerns without modifying handler code.

In [`services/document_storage_service/src/api/mod.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/api/mod.rs):

```rust
let app = axum::Router::new()
    .route("/", get(root_handler))
    .layer(
        ServiceBuilder::new()
            .layer(TraceLayer::new_for_http())
            .layer(CompressionLayer::new())
            .into_inner(),
    );

```

Layers stack in order: tracing, authentication, compression, rate limiting—each unaware of the others.

## Async Trait Pattern

Rust's lack of native async traits is bridged using the `async_trait` crate. This adapter pattern enables asynchronous behavior in port definitions, as seen in [`services/connection_gateway/src/service/connection.rs`](https://github.com/macro-inc/macro/blob/main/services/connection_gateway/src/service/connection.rs):

```rust
#[async_trait]
pub trait ConnectionRepo {
    async fn establish(&self, params: ConnectionParams) -> Result<Connection>;
    async fn close(&self, id: ConnectionId) -> Result<()>;
}

```

The `#[async_trait]` macro transforms these into trait objects compatible with Rust's type system, essential for the port-and-adapter design in an async runtime.

## Factory Pattern

Complex object construction is encapsulated in small factories. The IndexedDB storage layer in [`crates/client/cache-idb/src/idb_storage.rs`](https://github.com/macro-inc/macro/blob/main/crates/client/cache-idb/src/idb_storage.rs) uses:

```rust
impl IdbStorage {
    pub fn new(name: &str) -> Result<Self> {
        // complex initialization logic
        Ok(Self { db: init_db(name)? })
    }
}

```

Factories hide construction complexity and provide named constructor variants for different use cases.

## Command-Query Separation (CQS)

Service methods split strictly between **commands** (mutating) and **queries** (read-only), often organized in separate modules. The delete chat handler in [`services/delete_chat_handler/src/service/db/delete_chat.rs`](https://github.com/macro-inc/macro/blob/main/services/delete_chat_handler/src/service/db/delete_chat.rs) illustrates this:

```rust
// Command: delete operation
pub async fn delete_chat(repo: &impl ChatRepository, id: ChatId) -> Result<()>;

// Query: read operation (separate module)
pub async fn fetch_chat_metadata(repo: &impl ChatRepository, id: ChatId) -> Result<Metadata>;

```

This separation prevents side effects in read operations and clarifies intent at the call site.

## Observer / Pub-Sub Pattern

Event-driven components implement the observer pattern through message stream subscriptions. The email service workers in [`services/email_service/src/pubsub/link_manager/process.rs`](https://github.com/macro-inc/macro/blob/main/services/email_service/src/pubsub/link_manager/process.rs) demonstrate:

```rust
pub async fn process_messages(subscription: impl MessageStream) {
    while let Some(msg) = subscription.next().await {
        handle_event(msg).await;
    }
}

```

Workers react to external events without polling, enabling efficient, reactive system behavior.

## Summary

- **Port-and-adapter architecture** isolates domain logic from infrastructure through trait-defined ports
- **Repository pattern** abstracts all data access behind storage-agnostic interfaces
- **Fluent builders** provide readable, chainable APIs for complex object construction
- **Trait-based dependency injection** enables test doubles and implementation flexibility
- **Decorator middleware stacks** compose cross-cutting concerns via `tower::ServiceBuilder`
- **Async traits** bridge Rust's type system limitations for async port definitions
- **Factories** encapsulate complex initialization logic
- **Command-query separation** enforces clear semantics for read versus write operations
- **Observer/pub-sub** enables reactive, event-driven service interactions

## Frequently Asked Questions

### What makes the macro codebase testable?

The extensive use of **trait-based dependency injection** allows any concrete implementation to be replaced with mocks or fakes. Since services depend only on trait bounds, test suites provide lightweight in-memory repositories rather than connecting to real databases. Repository traits, service ports, and client interfaces are all designed for swapability.

### Why does macro use hexagonal architecture specifically?

Hexagonal architecture **protects domain logic from infrastructure churn**. The macro platform integrates with multiple storage systems (PostgreSQL, DynamoDB, IndexedDB), message queues, and third-party APIs. By depending only on ports (traits), the core business rules remain stable even as adapters evolve—critical for a platform handling enterprise document workflows across diverse deployment environments.

### How does the fluent builder pattern improve the developer experience?

The `FluentBuilder` trait in the xtask workflow crate transforms complex configuration into **readable, self-documenting code**. Rather than constructor calls with many positional parameters, developers write `Workflow::new().name("deploy").run("cargo build").on_push()`, where each method returns `self` for chaining. This pattern reduces errors and makes workflow definitions comprehensible at a glance.

### What role does tower play in the decorator pattern implementation?

The `tower` crate provides the **Service trait and ServiceBuilder infrastructure** that makes middleware composition ergonomic. Each layer (tracing, compression, authentication) implements `Service`, wrapping the inner service and intercepting requests/responses. The `ServiceBuilder` DSL in [`services/document_storage_service/src/api/mod.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/api/mod.rs) enables declarative, ordered layer stacking without manual service nesting.