{"post":{"slug":"ports-and-adapters-in-practice-2026","title":"Ports and Adapters in Practice: A 2026 Walkthrough","excerpt":"Hexagonal architecture is easy to diagram and hard to introduce into a real codebase. Here is a concrete walkthrough of one port, two adapters, and how to roll the pattern out incrementally.","category":"code","date":"August 7, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">M</span>ost teams that read about ports and adapters understand the diagram and then stall on the first line of code. The practical version of the pattern is smaller than the diagram suggests: pick one dependency your domain code currently reaches into directly, define an interface for exactly what the domain needs from it, and write a real implementation and a fake implementation behind that interface. Everything else, the composition root, the incremental rollout, the discipline about what counts as a seam, follows from getting that first one right. The value shows up the first time a database migration, a vendor swap, or a slow integration test suite no longer requires touching business logic at all.\n\n## How it works, concretely\n\nA port is an interface the domain owns, describing what it needs without naming any technology:\n\n```ts\n// Port: owned by the domain, describes what it needs\ninterface SessionStore {\n  get(sessionId: string): Promise<Session | null>;\n  save(session: Session): Promise<void>;\n}\n\n// Adapter 1: real implementation backed by a cookie-based session store\nclass CookieSessionStore implements SessionStore {\n  async get(sessionId: string) { /* read from the cookie-backed store */ }\n  async save(session: Session) { /* write it back */ }\n}\n\n// Adapter 2: test double, no infrastructure at all\nclass InMemorySessionStore implements SessionStore {\n  private data = new Map<string, Session>();\n  async get(sessionId: string) { return this.data.get(sessionId) ?? null; }\n  async save(session: Session) { this.data.set(session.id, session); }\n}\n```\n\nA use case such as `renewSession` depends only on `SessionStore`, never on cookies or a database directly. If the storage mechanism itself needs explaining to a newer engineer, see [what a cookie actually is](/blog/what-is-a-cookie-in-web-2026); the port hides exactly that detail from the domain. A generic `Repository<T>` port is just as common — if your language supports it, [how TypeScript generics work](/blog/typescript-generics-explained-2026) is worth reading before you write one interface that covers many entity types instead of duplicating it per entity. Whichever shape the port takes, the test for whether the boundary is right is simple: can you write a full unit test for the use case without starting a database, a browser, or a network call? If not, something from an adapter has leaked into the core.\n\n## Introducing it into an existing codebase\n\n1. **Find the seam that hurts most in tests today.** Usually the database or an external API call.\n2. **Define the port from the call site's perspective, not the implementation's.** Write the interface you wish already existed.\n3. **Extract the current code behind that interface as your first real adapter.** Nothing behaviorally changes yet.\n4. **Add an in-memory or fake adapter and switch your slowest tests to use it.**\n5. **Repeat on the next painful seam.** Do not attempt to wrap an entire codebase in one pass; if you want to try the pattern without risk, prototype it on [a fork](/blog/what-is-a-git-fork-2026) or a throwaway branch first.\n6. **Measure the payoff, not just the effort.** Track how much faster the tests behind that seam run, and use that number as evidence for whether the next seam is worth wrapping too.\n\n## Common mistakes\n\n- **Wrapping everything in a port on day one.** Most of a typical codebase is not a seam worth abstracting; over-application adds indirection with no test or flexibility benefit.\n- **Letting a framework type leak into the port signature.** If the interface imports an ORM entity or an HTTP framework type, the abstraction is not actually decoupling anything.\n- **Skipping the fake adapter.** The in-memory implementation is what makes the domain fast and easy to test; without it, you built the interface but not the payoff.\n- **Wiring adapters inside business logic.** Instantiating a concrete adapter inside a use case, instead of receiving it as a dependency, reintroduces the coupling the port was supposed to remove.\n\n## FAQ\n\n**Do I need a dependency injection framework to do this?**\nNo. A composition root can be a plain function that constructs adapters and passes them in; a DI container is a convenience, not a requirement.\n\n**How many ports should a typical service have?**\nAs many as it has real external seams, usually a handful: persistence, external APIs, the clock, maybe a queue or file store.\n\n**Is this the same as the repository pattern?**\nA repository is a common example of a port specifically for persistence; ports and adapters is the more general idea applied to every external dependency.\n\n**Can I retrofit this onto a legacy codebase?**\nYes, incrementally, one seam at a time, which is usually far more realistic than a rewrite.\n\n**What is the first seam most teams should pick?**\nWhatever currently makes the test suite slowest or flakiest, usually the database or an external HTTP call, since that is where the payoff from a fake adapter shows up fastest and most visibly.\n\n## Where to go next\n\nSee [clean architecture vs hexagonal](/blog/clean-architecture-vs-hexagonal-2026) for how this pattern relates to its closest sibling, [domain-driven design explained](/blog/domain-driven-design-explained-2026) for how to decide what belongs inside the boundary this protects, and [how TypeScript generics work](/blog/typescript-generics-explained-2026) for writing reusable port interfaces.","hue":310,"metaTitle":"Ports and Adapters in Practice 2026","metaDescription":"Ports and adapters in practice for 2026 — a real port, two adapters, and the steps to introduce hexagonal architecture into an existing codebase.","keywords":["ports and adapters in practice 2026","hexagonal architecture example code","port and adapter interface pattern","applying hexagonal architecture","repository pattern typescript example","composition root dependency injection","testable domain layer example","introducing hexagonal architecture","ports and adapters typescript"],"tldr":["**Start with one seam, not the whole codebase** — pick the single hardest-to-test dependency and wrap it in a port first.","**A port is just an interface your domain code owns** — it lives next to the business logic, not next to the database client it will eventually replace.","**Write the test-double adapter before the real one** — an in-memory adapter proves the port's shape is usable and unblocks fast unit tests immediately.","**Wire adapters at the edge, not in the middle** — a composition root is the only place that should know which concrete adapter is in use.","**Skip:** creating a port for every single class for consistency — ports earn their cost at real seams like the database or an external API, not for pure in-memory helpers."],"readTime":"4 min read","wordCount":838},"related":[{"slug":"hexagonal-architecture-explained-2026","title":"Hexagonal Architecture Explained in 2026","excerpt":"Hexagonal architecture keeps business logic independent of any specific database, framework, or UI. Here is what ports and adapters actually mean, and when it pays off.","category":"code","date":"July 2, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":150},{"slug":"clean-architecture-vs-hexagonal-2026","title":"Clean Architecture vs Hexagonal: 2026 Comparison","excerpt":"Clean architecture and hexagonal architecture solve the same problem with different diagrams. Here is where they actually differ in 2026 and how to decide which vocabulary to standardize on.","category":"code","date":"July 31, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":312},{"slug":"dependency-injection-explained-2026","title":"Dependency injection explained in 2026: from concept to production","excerpt":"Dependency injection is the pattern behind every testable, maintainable service. This 2026 guide covers the concept, DI containers, and when to skip them entirely.","category":"code","date":"March 10, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":320}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"platform-engineering-explained-2026","title":"Platform Engineering Explained in 2026: What It Is","excerpt":"Platform engineering builds internal infrastructure as a self-service product. Here is what these teams actually own and how they differ from DevOps.","category":"code","date":"August 7, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":308},"older":{"slug":"probate-explained-2026","title":"Probate Explained: What Happens Without a Trust in 2026","excerpt":"Without a living trust, most estates go through probate, a court-supervised process to pay debts and distribute assets. Here is what it costs and how long it takes.","category":"finance","date":"August 7, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":281}}