# Hexagonal Architecture Patterns in Macro's Rust Services: A Complete Implementation Guide

> Master hexagonal architecture in Rust services. Learn Macro's pattern for domain-centric ports, adapter implementations, and dependency wiring for robust applications.

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

---

**Macro's Rust services implement hexagonal architecture by defining domain-centric port traits in [`src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/src/domain/ports.rs), providing concrete adapter implementations in `src/outbound/` modules, and wiring dependencies at the crate's composition root in [`src/lib.rs`](https://github.com/macro-inc/macro/blob/main/src/lib.rs).**

The [macro-inc/macro](https://github.com/macro-inc/macro) repository demonstrates production-grade hexagonal architecture (ports and adapters) across its service crates. This pattern separates pure business logic from infrastructure concerns using Rust's trait system, enabling testable, modular services. The notification crate (`crates/notification`) serves as the canonical example, with consistent patterns repeated across contacts, documents, and other service crates.

## Core Hexagonal Architecture Layers in Macro

### Domain Layer: Pure Business Logic

The **domain layer** contains business rules, data structures, and use-case orchestration. It depends only on abstract **port traits**, never on concrete implementations.

In [`crates/notification/src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/crates/notification/src/domain/ports.rs), the domain defines contracts for capabilities it needs:

```rust
/// Port used by the domain to send notifications.
pub trait NotificationSender: Send + Sync + 'static {
    fn send(&self, notification: Notification) -> Result<(), NotificationError>;
}

/// Port used by the domain to persist notifications.
pub trait NotificationRepository: Send + Sync + 'static {
    fn save(&self, notification: Notification) -> Result<(), RepositoryError>;
}

```

The `Send + Sync + 'static` bounds ensure adapters are safe for async runtimes. These traits represent **outbound ports**—capabilities the domain requires from the outside world.

### Port Traits: Dependency Inversion Boundaries

**Ports** are Rust traits that describe services the domain needs. They are **dependency-inverted**: the domain *declares* the contract, and infrastructure *fulfills* it.

Key characteristics of Macro's port definitions:
- Located in [`src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/src/domain/ports.rs) within each crate
- Use async-trait for asynchronous operations
- Include error types defined in the domain
- Remain agnostic to implementation technology

### Adapter Layer: Infrastructure Implementations

**Adapters** live in `src/outbound/` (for outbound ports) and `src/inbound/` (for inbound ports). They translate between domain concepts and external systems.

In [`crates/notification/src/outbound/email.rs`](https://github.com/macro-inc/macro/blob/main/crates/notification/src/outbound/email.rs):

```rust
pub struct EmailServiceOpsImpl {
    client: reqwest::Client,
    config: EmailConfig,
}

#[async_trait::async_trait]
impl EmailServiceOps for EmailServiceOpsImpl {
    async fn send_email(&self, payload: EmailPayload) -> Result<(), EmailError> {
        // concrete logic that talks to an SMTP gateway or third‑party API
        self.client.post(&self.config.endpoint)
            .json(&payload)
            .send()
            .await?
            .error_for_status()?;
        Ok(())
    }
}

```

The implementation owns infrastructure resources (`reqwest::Client`) while satisfying the domain's port contract.

Additional adapter examples in the notification crate:
- [`src/outbound/websocket.rs`](https://github.com/macro-inc/macro/blob/main/src/outbound/websocket.rs): `WebSocketGatewayOpsImpl` implements `WebSocketGatewayOps`
- [`src/outbound/sns_endpoint.rs`](https://github.com/macro-inc/macro/blob/main/src/outbound/sns_endpoint.rs): AWS SNS integration adapter

### Composition Root: Wiring Dependencies

The **composition root** in [`crates/notification/src/lib.rs`](https://github.com/macro-inc/macro/blob/main/crates/notification/src/lib.rs) assembles the service by injecting concrete adapters:

```rust
pub fn build_notification_service(
    email_impl: impl EmailServiceOps + Send + Sync + 'static,
    ws_impl: impl WebSocketGatewayOps + Send + Sync + 'static,
) -> NotificationService {
    NotificationService::new(
        Box::new(email_impl),
        Box::new(ws_impl),
        // other adapters …
    )
}

```

This function accepts **impl Trait** parameters for flexibility, then boxes them into trait objects for storage. The domain service receives `Box<dyn PortTrait>` types, remaining decoupled from specific implementations.

## Hexagonal Architecture Testing Patterns

### Mock Adapters for Unit Testing

Domain tests use **mock adapters** implementing the same ports, avoiding real infrastructure calls. In [`crates/notification/src/domain/test.rs`](https://github.com/macro-inc/macro/blob/main/crates/notification/src/domain/test.rs):

```rust
#[derive(Default)]
struct MockNotificationSender {
    pub sent: Mutex<Vec<Notification>>,
}

#[async_trait::async_trait]
impl NotificationSender for MockNotificationSender {
    async fn send(&self, notification: Notification) -> Result<(), NotificationError> {
        self.sent.lock().unwrap().push(notification);
        Ok(())
    }
}

```

This pattern enables:
- Deterministic, fast tests
- Verification of side effects via `sent` recordings
- Testing error scenarios by configuring mocks to fail

### Domain Service Testing

The [`src/domain/service/push_notification_event/mod.rs`](https://github.com/macro-inc/macro/blob/main/src/domain/service/push_notification_event/mod.rs) orchestrates use cases using injected ports. Tests pass mock implementations to verify business rules in isolation.

## Consistent Patterns Across Service Crates

Macro applies these hexagonal architecture patterns uniformly:

| Crate | Domain Service | Key Ports | Notable Adapters |
|-------|---------------|-----------|------------------|
| `crates/notification` | `push_notification_event` | `NotificationSender`, `NotificationRepository`, `EmailServiceOps`, `WebSocketGatewayOps` | Email service, WebSocket gateway, SNS endpoint |
| `crates/contacts` | Contact management | Contact persistence ports | Database repository adapters |
| `crates/documents` | `markdown_backfill` | Document processing ports | Background job adapters, storage backends |

The [`crates/documents/src/domain/markdown_backfill.rs`](https://github.com/macro-inc/macro/blob/main/crates/documents/src/domain/markdown_backfill.rs) demonstrates hexagonal patterns in background processing services, using ports for CPU-intensive domain operations.

## Why Hexagonal Architecture Works in Rust

**Zero-cost abstractions**: Trait objects (`Box<dyn NotificationSender>`) compile to thin v-tables with negligible overhead.

**Explicit ownership**: Adapters own resources (HTTP clients, database connections) while the domain remains pure.

**Type-safe dependency injection**: The compiler verifies that injected adapters satisfy port contracts.

**Composability**: `impl Trait` parameters in composition roots enable flexible assembly without runtime overhead.

## Summary

- **Define ports as traits** in [`src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/src/domain/ports.rs) with `Send + Sync + 'static` bounds for async compatibility
- **Implement adapters** in `src/outbound/` or `src/inbound/` modules, owning infrastructure resources
- **Wire at composition root** in [`src/lib.rs`](https://github.com/macro-inc/macro/blob/main/src/lib.rs) using constructor injection with `impl Trait` or `Box<dyn Trait>`
- **Test with mock adapters** implementing the same ports for isolated, deterministic domain tests
- **Apply consistently** across all service crates (notification, contacts, documents) for architectural uniformity

## Frequently Asked Questions

### How does Macro's hexagonal pattern handle async operations?

The codebase uses `async-trait` to define asynchronous port methods. Port traits include `Send + Sync + 'static` bounds, ensuring adapters work with Tokio and other async runtimes. Adapter implementations use `.await` for I/O operations while domain services remain agnostic to the executor.

### Can I swap infrastructure implementations without changing domain code?

Yes. Because domain code depends only on trait definitions in [`src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/src/domain/ports.rs), you can replace `EmailServiceOpsImpl` with a different email provider implementation without modifying `NotificationService` or its tests. The composition root in [`src/lib.rs`](https://github.com/macro-inc/macro/blob/main/src/lib.rs) is the sole location requiring changes.

### Where should I place business logic in Macro's pattern?

Business logic belongs in **domain services** like [`push_notification_event/mod.rs`](https://github.com/macro-inc/macro/blob/main/push_notification_event/mod.rs), not in adapters. Domain services orchestrate use cases by calling port methods, applying business rules to the results. Keep adapters thin—they translate data formats and call external systems, containing minimal decision logic.

### How does Macro handle cross-crate dependencies in this architecture?

Each crate maintains its own hexagonal boundaries with internal `domain/`, `outbound/`, and `inbound/` modules. Crates expose minimal public APIs through [`src/lib.rs`](https://github.com/macro-inc/macro/blob/main/src/lib.rs). When services need to collaborate, they depend on other crates' domain types and ports, not their implementations, preserving architectural isolation.