Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Enforce hexagonal architecture in the Rust backend. Use before modifying crates or Rust services, especially inbound axum/tool/listener adapters, domain services/ports, outbound adapters, authorization, permissions, database access, or external clients.
.claude/skills/macro-inc-cloud-storage-hexagonal-architecture/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 70% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 170% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 94% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 5% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 72% | 0% |
Use this skill whenever you add, change, or review Rust code under crates/** or services/** that touches a crate with src/domain, src/inbound, or src/outbound.
This repository follows the ports-and-adapters / hexagonal style described in _Master Hexagonal Architecture in Rust_ and the howtocodeit/hexarch 3-simple-service branch: domain models + ports + services are the center; inbound and outbound adapters are replaceable shells around that center.
Dependencies point inward:
textinbound adapters ──► domain ports/models/services ◄── outbound adapters composition root ──► inbound + domain service + outbound implementations
domain/ must not depend on inbound/, outbound/, axum, HTTP response types, SQLx pools/queries, AWS SDKs, Redis, reqwest, environment variables, or transport DTOs.inbound/ may depend on domain ports/models/services. It must not own business decisions or persistence/external-service implementation details.outbound/ implements domain ports for databases, S3, HTTP clients, queues, metrics, etc. It must not own use-case policy.src/domain/**)Put the following here:
src/inbound/**)Axum handlers, AI tools, Kafka/listener handlers, lambda handlers, and CLI entrypoints are adapters. Keep them thin:
MacroAuthorizationExtractor, OptionalMacroAuthorizationExtractor, JWT, signed internal header, request context).Inbound adapters must not:
entity_access, roles_and_permissions, repositories, SQLx, S3, Redis, SQS, reqwest, or other outbound implementations to make a use-case decision.AccessLevel, role, owner/admin/member, tenant/team membership, project membership, subscription tier, feature entitlement, entity state, or ownership for business policy.src/outbound/**)Put implementation details here:
Outbound adapters must not:
crate::inbound::*, axum extractors/responses, or transport DTOs.EntityAccessReceipt ruleAuthentication can happen at the edge. Entity access checks should cross the boundary as a typed capability: EntityAccessReceipt<T>.
EntityAccessReceipt<T> means the entity access layer has verified the caller has at least permission T for the entity. Inbound adapters may obtain this receipt through the standard access extractors or by calling the entity access service specifically to mint a receipt. After that, ordinary handlers/tools/listeners must pass the receipt inward instead of re-checking or branching on authorization.
Allowed in inbound:
401 / unauthenticated).actor, request_context, user_id, service identity, internal principal, or a typed EntityAccessReceipt<T>.generate_entity_access_receipt::<RequiredLevel>(...) to mint a receipt.Forbidden in ordinary inbound handlers/tools/listeners:
if user_id != owner_id { ... }if access_level < Edit { ... }entity_access_service.get_access_level(...) or can_edit(...) followed by allow/deny branching.receipt.entity_permission() to decide use-case business policy.Correct pattern:
ViewAccessLevel, EditAccessLevel, or OwnerAccessLevel.EntityAccessReceipt<RequiredLevel> using the existing entity access boundary.Unauthorized, Forbidden, or a typed policy error.Bad: the handler branches on permissions and performs the protected action itself.
rustpub async fn edit_document_handler( State(state): State<DocumentRouterState>, Json(args): Json<EditDocumentServiceArgs>, ) -> Result<Json<EditDocumentResponse>, DocumentError> { let access_level = state .entity_access .get_access_level(current_user(), &args.document_id, EntityType::Document) .await? .ok_or(DocumentError::Unauthorized)?; if access_level < AccessLevel::Edit { return Err(DocumentError::Unauthorized); } if args.share_permission.is_some() && access_level != AccessLevel::Owner { return Err(DocumentError::Unauthorized); } state.document_repo.update_document(args).await?; Ok(Json(EditDocumentResponse::success())) }
Good: inbound obtains a typed receipt and forwards it; the domain service owns policy and orchestration.
rustpub async fn edit_document_handler<T: DocumentService, Svc: EntityAccessService>( access: DocumentAccessExtractor<EditAccessLevel, Svc>, State(state): State<DocumentRouterState<T, Svc>>, document_context: LoadedDocumentBasic, project: ProjectBodyAccessLevelExtractor<EditAccessLevel, EditDocumentServiceArgs, Svc>, ) -> Result<Json<EditDocumentResponse>, DocumentError> { state .service .edit_document( access.entity_access_receipt, document_context.into_inner(), project.into_inner(), ) .await?; Ok(Json(EditDocumentResponse::success())) }
rustasync fn edit_document( &self, receipt: EntityAccessReceipt<EditAccessLevel>, document_context: DocumentBasic, args: EditDocumentServiceArgs, ) -> Result<(), DocumentError> { if let EntityPermission::AccessLevel { access_level } = receipt.entity_permission() { if args.project_id.is_some() && *access_level != AccessLevel::Owner { return Err(DocumentError::Unauthorized); } if args.share_permission.is_some() && *access_level != AccessLevel::Owner { return Err(DocumentError::Unauthorized); } } let document_id = receipt.entity().entity_id.clone(); self.repo .edit_document_metadata(document_id, document_context, args) .await }
Before editing code, classify each touched file:
domain, inbound, outbound, or composition/wiring?If a step has no answer, stop and design that boundary before writing code.
For every Rust backend diff, reject or refactor if any of these are true:
src/domain/** imports axum, http::StatusCode, IntoResponse, Json, Router, Request, HeaderMap, SQLx pools/queries, AWS SDK clients, Redis clients, reqwest clients, crate::inbound, or crate::outbound.src/inbound/** contains SQLx queries, transaction handling, repository calls, AWS/Redis/OpenSearch/reqwest calls, or direct calls to outbound implementations.src/inbound/** handlers/tools/listeners contain authorization decisions (AccessLevel, role checks, owner checks, team/project membership checks, can_*, authorize_*, ensure_*permission*) instead of forwarding a typed EntityAccessReceipt<T> or identity to a service. Dedicated access extractors whose job is to mint receipts are the exception.Set CRATE to the crate you are touching, for example CRATE=crates/documents.
bash# Domain must not know transport or concrete infrastructure. rg -n "use (axum|http::StatusCode)|IntoResponse|Json<|Router|HeaderMap|Request<|sqlx::|PgPool|aws_sdk|redis::|reqwest|crate::inbound|crate::outbound" "$CRATE/src/domain" --glob '*.rs' # Inbound authz/policy hits require inspection. Receipt-minting extractors are allowed; # ordinary handlers should forward EntityAccessReceipt<T> instead of branching. rg -n "entity_access|EntityAccessReceipt|roles_and_permissions|AccessLevel|RoleId|owner|admin|member|tenant|team|project|permission|authorize|authz|can_|ensure_.*permission|Forbidden|Unauthorized" "$CRATE/src/inbound" --glob '*.rs' # Inbound should not do persistence or infrastructure work. rg -n "sqlx::|query!|query_as!|PgPool|Transaction|aws_sdk|redis::|opensearch|reqwest|S3|Sqs|Dynamo" "$CRATE/src/inbound" --glob '*.rs' # Outbound must not depend on inbound transport. rg -n "crate::inbound|axum|IntoResponse|Json<|Router|StatusCode" "$CRATE/src/outbound" --glob '*.rs'
rg hits are not automatically failures, but every hit must be explained by layer responsibilities. When in doubt, move policy inward.
When you use this skill, explicitly state that the hexagonal boundary was checked and summarize where authz/business policy lives after your change.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 28,328 | 35,916 | +27% | 1 | 1 | 0% | 5,242 | 8,924 | +70% | 0 | 0 | — |
case-02 | fail→fail | 32,899 | 12,227 | -63% | 1 | 1 | 0% | 5,709 | 3,028 | -47% | 0 | 0 | — |
case-03 | fail→pass | 25,915 | 34,598 | +34% | 1 | 1 | 0% | 3,608 | 9,726 | +170% | 0 | 0 | — |
case-04 | fail→pass | 15,672 | 14,986 | -4% | 1 | 1 | 0% | 2,940 | 5,701 | +94% | 0 | 0 | — |
case-05 | fail→pass | 29,882 | 21,459 | -28% | 1 | 1 | 0% | 6,753 | 7,086 | +5% | 0 | 0 | — |
case-06 | pass→pass | 18,170 | 24,985 | +38% | 1 | 1 | 0% | 3,311 | 4,989 | +51% | 0 | 0 | — |
case-07 | fail→pass | 19,169 | 16,352 | -15% | 1 | 1 | 0% | 3,314 | 5,701 | +72% | 0 | 0 | — |
case-08 | pass→fail | 15,693 | 6,213 | -60% | 1 | 1 | 0% | 2,859 | 3,071 | +7% | 0 | 0 | — |
case-09 | pass→pass | 16,868 | 18,864 | +12% | 1 | 1 | 0% | 3,075 | 7,009 | +128% | 0 | 0 | — |
case-10 | pass→pass | 16,088 | 12,635 | -21% | 1 | 1 | 0% | 3,283 | 5,379 | +64% | 0 | 0 | — |
case-11 | pass→pass | 18,254 | 18,538 | +2% | 1 | 1 | 0% | 3,629 | 6,432 | +77% | 0 | 0 | — |
case-12 | pass→pass | 19,297 | 17,853 | -7% | 1 | 1 | 0% | 3,607 | 5,950 | +65% | 0 | 0 | — |
case-13 | pass→pass | 13,624 | 13,125 | -4% | 1 | 1 | 0% | 2,585 | 5,226 | +102% | 0 | 0 | — |
case-14 | pass→pass | 16,873 | 15,667 | -7% | 1 | 1 | 0% | 3,036 | 5,954 | +96% | 0 | 0 | — |
case-15 | pass→pass | 36,175 | 6,835 | -81% | 1 | 1 | 0% | 2,254 | 3,857 | +71% | 0 | 0 | — |
case-16 | fail→pass | 10,857 | 6,376 | -41% | 1 | 1 | 0% | 1,322 | 4,051 | +206% | 0 | 0 | — |
case-17 | fail→pass | 15,860 | 6,358 | -60% | 1 | 1 | 0% | 2,377 | 3,806 | +60% | 0 | 0 | — |
case-18 | pass→pass | 16,509 | 13,005 | -21% | 1 | 1 | 0% | 3,008 | 5,245 | +74% | 0 | 0 | — |
case-19 | pass→pass | 11,919 | 21,771 | +83% | 1 | 1 | 0% | 2,168 | 5,667 | +161% | 0 | 0 | — |
case-20 | pass→fail | 8,042 | 7,974 | -1% | 1 | 1 | 0% | 1,375 | 3,935 | +186% | 0 | 0 | — |
case-21 | pass→pass | 14,727 | 13,021 | -12% | 1 | 1 | 0% | 2,718 | 5,523 | +103% | 0 | 0 | — |
case-22 | pass→pass | 19,969 | 25,793 | +29% | 1 | 1 | 0% | 3,193 | 8,286 | +160% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 20 counted toward the lift figure. The other 2 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +23 percentage points is the difference between those two pass rates over the 20 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.