# How the Macro Backend Implements Hexagonal Architecture

> Discover how the Macro backend leverages hexagonal architecture in Rust to isolate domain logic from infrastructure concerns. Learn about ports and adapters for clean service structure.

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

---

**The Macro backend structures each Rust service crate following hexagonal (ports-and-adapters) architecture, isolating domain logic from HTTP handlers and database implementations through explicit trait-based ports.**

The open-source macro-inc/macro repository demonstrates enterprise-grade software design in Rust. By organizing services into independent crates with clear domain, inbound, and outbound layers, the system achieves loose coupling that allows PostgreSQL storage to be swapped for Redis, HTTP APIs to be replaced with gRPC, and core business rules to be unit-tested without real infrastructure.

## Three-Layer Crate Structure

Every service in the Macro workspace follows a consistent physical layout that reflects hexagonal architecture principles. Each crate contains three logical layers that communicate through well-defined interfaces rather than concrete dependencies.

- **Domain Layer**: Located at `crates/<service>/src/domain/`, this layer holds pure Rust types, business rules, and **port** traits that declare operations the service requires. For example, in [`crates/projects/src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/crates/projects/src/domain/ports.rs), the `ProjectRepo` trait defines methods like `get_basic_project`, `create_project`, and `soft_delete_project` without referencing concrete database types such as `PgPool`.

- **Inbound Adapters**: Found in `crates/<service>/src/inbound/`, these thin controllers translate external protocols into domain operations. The Projects service exposes its HTTP API in [`crates/projects/src/inbound/axum_router.rs`](https://github.com/macro-inc/macro/blob/main/crates/projects/src/inbound/axum_router.rs), handling validation and authentication before delegating to domain services.

- **Outbound Adapters**: Stored in `crates/<service>/src/outbound/`, these implement domain ports against concrete infrastructure. The PostgreSQL implementation of `ProjectRepo` lives in [`crates/projects/src/outbound/pg_project_repo.rs`](https://github.com/macro-inc/macro/blob/main/crates/projects/src/outbound/pg_project_repo.rs), keeping all SQL and I/O concerns outside the core logic.

## Domain Ports and Traits

At the heart of the Macro backend hexagonal architecture are **ports**: Rust traits that describe behaviors the domain requires from the outside world. These traits deliberately contain no concrete infrastructure types, specifying only the contract expected.

In [`crates/projects/src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/crates/projects/src/domain/ports.rs), the `ProjectRepo` trait defines the persistence contract:

```rust
#[cfg_attr(test, mockall::automock)]
pub trait ProjectRepo {
    type Err;
    async fn get_basic_project(&self, project_id: &str) -> Result<Option<BasicProject>, Self::Err>;
    async fn create_project(&self, project: &NewProject) -> Result<Project, Self::Err>;
    async fn soft_delete_project(&self, project_id: &str) -> Result<(), Self::Err>;
}

```

The `#[cfg_attr(test, mockall::automock)]` attribute enables automatic mock generation, allowing unit tests to verify business logic without requiring a real PostgreSQL instance. This isolation ensures that domain code remains pure and testable.

## Inbound and Outbound Adapters

Adapters bridge the domain ports to the external world. **Inbound adapters** handle protocol translation from HTTP, gRPC, or CLI, while **outbound adapters** implement port traits against specific infrastructure.

The inbound HTTP adapter in [`crates/projects/src/inbound/axum_router.rs`](https://github.com/macro-inc/macro/blob/main/crates/projects/src/inbound/axum_router.rs) validates JSON payloads, extracts user IDs, and calls domain service methods. It contains no business logic—only protocol handling and authentication.

Outbound adapters like `PgProjectRepo` in [`crates/projects/src/outbound/pg_project_repo.rs`](https://github.com/macro-inc/macro/blob/main/crates/projects/src/outbound/pg_project_repo.rs) implement the `ProjectRepo` trait using `sqlx` and PostgreSQL connections. Because the domain depends only on the trait, swapping PostgreSQL for another datastore requires only a new adapter without modifying domain code.

## Service Layer and Dependency Injection

The **service layer** orchestrates business rules by composing ports. Located in the domain layer, structs like `ProjectService` receive trait objects implementing the required ports, ensuring they remain independent from HTTP or database specifics.

At application startup, concrete adapters are instantiated and injected into the service implementation. For example, in the service's [`main.rs`](https://github.com/macro-inc/macro/blob/main/main.rs):

```rust
let pg_repo = PgProjectRepo::new(pg_pool.clone());
let redis_repo = RedisProjectRepo::new(redis_client.clone());

let service = ProjectServiceImpl::new(
    pg_repo,               // primary storage
    redis_repo,            // cache layer (implements same ProjectRepo trait)
    upload_url_port,
    search_indexer,
);

```

The service receives only abstract port traits, guaranteeing testability and loose coupling. The domain orchestrates the flow without knowing how the database or HTTP layer works—it only knows what it can ask for.

## Multi-Target Deployment with Feature Gates

The Macro backend leverages Cargo features to compile the same domain logic for different runtime environments. Crates expose optional inbound and outbound modules via features named `inbound` and `outbound`.

For deployment targets that lack upload functionality (such as AI-tool-only hosts), the system provides stub implementations like `UnavailableProjectUploadUrlPort`. These stubs implement the port trait but fail fast at runtime, allowing the code to compile for restricted environments while maintaining the same API surface.

## End-to-End Example: Project Creation

Consider the flow when creating a new project:

1. An HTTP request hits the **inbound** router at the `create_project` endpoint in [`crates/projects/src/inbound/axum_router.rs`](https://github.com/macro-inc/macro/blob/main/crates/projects/src/inbound/axum_router.rs).
2. The router validates the payload and calls `ProjectService::create_project`.
3. `ProjectService` invokes the `ProjectRepo` port, delegating to `PgProjectRepo::create_project` to execute a SQL transaction.
4. After the transaction succeeds, the service may call `ProjectUploadUrlPort` to generate presigned URLs or `ProjectSearchIndexer` to schedule index updates.

All steps are wired together without the domain knowing the concrete implementations.

## Extending the Architecture

Adding new infrastructure requires only new adapters. For example, implementing a Redis cache involves creating a struct that implements the existing `ProjectRepo` trait:

```rust
// src/outbound/redis_cache.rs
use crate::domain::ports::ProjectRepo;
use redis::AsyncCommands;

#[derive(Clone)]
pub struct RedisProjectRepo {
    client: redis::Client,
}

#[async_trait::async_trait]
impl ProjectRepo for RedisProjectRepo {
    type Err = redis::RedisError;

    async fn get_basic_project(&self, project_id: &str)
        -> Result<Option<BasicProject>, Self::Err>
    {
        let mut conn = self.client.get_async_connection().await?;
        let json: Option<String> = conn.get(format!("project:{project_id}")).await?;
        Ok(json.map(|s| serde_json::from_str(&s).unwrap()))
    }
    // Additional methods delegate to PostgreSQL or are no-ops
}

```

This pattern repeats across the workspace. The `notification` crate organizes its ports in [`crates/notification/src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/crates/notification/src/domain/ports.rs), while `connection_gateway` and `scheduled_action` follow identical structures in their respective [`domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/domain/ports.rs) files.

## Summary

- **The Macro backend** organizes each service into domain, inbound, and outbound layers following hexagonal architecture principles.
- **Domain ports** are trait definitions in files like [`crates/projects/src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/crates/projects/src/domain/ports.rs) that specify behavior without concrete infrastructure dependencies.
- **Adapters** isolate protocol handling (inbound) and persistence logic (outbound), enabling storage swaps without domain changes.
- **Feature gates** allow compilation for different deployment targets using stub adapters like `UnavailableProjectUploadUrlPort`.
- **Dependency injection** at startup wires concrete implementations to domain services, ensuring testability with tools like `mockall`.

## Frequently Asked Questions

### What is the benefit of hexagonal architecture in the Macro backend?

Hexagonal architecture isolates business rules in the domain layer, allowing the Macro team to unit-test core logic with mock implementations generated by `mockall`. This separation enables changing PostgreSQL to Redis or adding Kafka producers without modifying domain code, reducing regression risks and accelerating feature development.

### How does the Macro backend handle different deployment environments?

The Macro backend uses Cargo feature flags (`inbound`, `outbound`) to conditionally compile adapter modules. For environments lacking upload capabilities, the system compiles stub implementations like `UnavailableProjectUploadUrlPort` that satisfy the port trait but return errors at runtime, ensuring type safety across all deployment targets.

### Where are the domain ports defined in the Macro repository?

Domain ports are defined as Rust traits in [`src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/src/domain/ports.rs) within each service crate. Key examples include [`crates/projects/src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/crates/projects/src/domain/ports.rs) containing the `ProjectRepo` trait, [`crates/notification/src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/crates/notification/src/domain/ports.rs) for notification operations, and [`crates/scheduled_action/src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/crates/scheduled_action/src/domain/ports.rs) for scheduled task management.

### How does dependency injection work in the Macro hexagonal architecture?

At application startup in [`main.rs`](https://github.com/macro-inc/macro/blob/main/main.rs), concrete adapter structs like `PgProjectRepo` and `RedisProjectRepo` are instantiated with their connection pools. These concrete instances are injected into the domain service constructor (e.g., `ProjectServiceImpl::new`), which accepts only abstract trait objects. This inversion of control ensures domain logic remains pure and infrastructure-agnostic.