| name | hexagonal |
| description | Ports & Adapters with Effect Context.Tag and Layer |
| allowed-tools | Read, Write, Edit, Grep |
| token-budget | 400 |
hexagonal
Core Concept
Hexagonal Architecture isolates business logic from infrastructure:
- Port = Context.Tag (interface definition)
- Adapter = Layer (implementation)
- Live adapters for production, Test adapters for tests
Define a Port
import { Context, Effect } from "effect";
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{
readonly findById: (id: UserId) => Effect.Effect<User, UserNotFoundError>;
readonly save: (user: User) => Effect.Effect<void, DatabaseError>;
}
>() {}
Create Adapters
import { Layer } from "effect";
// Live adapter - real database
const UserRepositoryLive = Layer.effect(UserRepository,
Effect.gen(function* () {
const db = yield* Database;
return {
findById: (id) => db.query("SELECT * FROM users WHERE id = ?", [id]),
save: (user) => db.execute("INSERT INTO users ...", [user]),
};
})
);
// Test adapter - in-memory
const UserRepositoryTest = Layer.succeed(UserRepository, {
findById: () => Effect.succeed(testUser),
save: () => Effect.succeed(undefined),
});
Testing Without Mocks
describe("createUser", () => {
it("saves user", async () => {
const TestLayer = Layer.mergeAll(UserRepositoryTest, ClockTest);
const result = await Effect.runPromise(
createUser({ name: "Test" }).pipe(Effect.provide(TestLayer))
);
expect(result.name).toBe("Test");
});
});
No mocks needed - swap layers for different behaviors.