Design Patterns in the macro-inc/macro Codebase: A Complete Architectural Guide
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, the webhook crate defines core port traits:
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:
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, this pattern enables readable, step-by-step workflow definition:
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:
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:
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:
#[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 uses:
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 illustrates this:
// 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 demonstrate:
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 enables declarative, ordered layer stacking without manual service nesting.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →