How we migrated our codebase from fp-ts to Effect
Tags: fpts, effect, migration, typescript
Published: 2024-06-04 14:30:41

## Summary

At Inato, we migrated from fp-ts to Effect in early 2024. Given our substantial codebase (around 500k lines of typescript code), we needed a way of ensuring any new code could be written using Effect while allowing existing fp-ts code to coexist. We achieved this goal in just two months, dedicating around 10% of our time to it. In this article, you will find our detailed migration strategy, the helpers we developed (which you can find in this [repository](https://github.com/inato/effect-fpts-interop)), and how we ensured a smooth transition of our codebase.

## Migrate to Effect, why?

At Inato we were very motivated early on to adopt functional programming, so we started using fp-ts in our codebase at the beginning of 2020. If you want to know more about this, have a look at [Our journey to functional programming](https://medium.com/inato/our-journey-to-functional-programing-36854a370de1).

Let’s now get to the heart of the matter: at the beginning of this year, we officially decided to switch to Effect! Why?

* The main maintainer of fp-ts (gcanti 👋) [joined the Effect team](https://dev.to/effect/a-bright-future-for-effect-455m) which presumably means less active development on the fp-ts side and positions Effect as a rather obvious next step.
* Because of the learning curve associated with fp-ts and the lack of documentation. Developers who joined Inato in the last years have frequently mentioned it: learning fp-ts is not really straightforward. This is a strong point for Effect with top-notch documentation and a lot of resources for training.
* For even more reasons, visit the [Effect website which compares fp-ts and Effect](https://effect.website/docs/other/fp-ts#comparison-table)!

Migrating our codebase to Effect is a great goal, but doing it turned out to be more challenging and required careful planning. We also wanted to limit the time spent on this project, so we agreed on a 2.5-month deadline. With all this in mind, we came up with the following strategy.

## The Migration Strategy

First of, here’s a representation of our server-side codebase: we have use cases that represent our business actions, these use cases have multiple dependencies (services, repositories, etc. — we’ll refer to them as ports), and we also have runners that will execute our use cases:

![Our codebase](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gg20e2lnwwjo8vlqrif5.png)

When we started the migration we had around 400 use cases and 80 ports and their adapters to migrate.

Our objective for this migration was clear: by the end of our 2.5-month window, any new use case or port will be written using Effect. To have a smooth transition that would allow us to have fp-ts and Effect code cohabitating, we came up with the following plan:

1. Ensure our ports return `ReaderTaskEither` to facilitate the transition to Effect \[\*\]
2. Create Effect proxies of our ports: only one implementation in fp-ts, but the ability to use an fp-ts “real” version and an Effect proxy version of each port
3. Start (re)writing use cases in Effect
4. Create fp-ts proxies of Effect use cases
5. Start (re)writing ports in Effect
6. Create fp-ts proxies of Effect ports: at this point, we would already have fulfilled our objective of writing new use cases and ports with Effect. But we wanted to go the extra mile to have the full flow covered!
7. Be able to run both Effect and fp-ts use cases

![Migration steps](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sr4pgwtg3gn2e5oy8n8p.png)

\[\*\] `ReaderTaskEither` (we will refer to it as `RTE` later on) was a prerequisite to facilitate the migration to Effect. Why? Conceptually, a `ReaderTaskEither` can be represented as follows:

```ts
ReaderTaskEither<R, E, A>  
= Reader<R, TaskEither<E, A>>  
= (context: R) => () => Promise<Either<E, A>>
```

If we look at the representation of an effect given on the [official Effect website](https://effect.website/docs/guides/essentials/the-effect-type), we can see that these are very similar concepts (which is something that we will leverage during our migration):

```ts
Effect<A, E, R> ~ (context: Context<R>) => E | A
```

## The Migration Process

Let’s deep dive into the code now! Here are the steps we are going to follow:

* [The program to migrate](#the-program-to-migrate)
* [Create Effect proxies of the ports](#create-effect-proxies-of-the-ports)
* [Rewrite a use case in Effect](#rewrite-a-use-case-in-effect)
* [Convert ports to Effect](#convert-ports-to-effect)
* [Use ManagedRuntime to run Effect usecases](#use-managedruntime-to-run-effect-usecases)
* [Bonus: simplify fp-ts ↔ effect tag mapping management](#bonus-simplify-fpts-%E2%86%94-effect-tag-mapping-management)

To illustrate our migration process, we will focus on an example program that is representative of how our codebase is organized.

_Note: all the code and helpers that will be presented are available in 👉_ [_this repository_](https://github.com/inato/effect-fpts-interop) _👈_

The program to migrate
----------------------

Let say that our domain model is composed of a simple `Foo` class:

```ts
// domain.ts  
export class Foo {  
  constructor(readonly id: string) {}  
  static make = (id = "random-id") => new Foo(id);  
}
```

We define a repository port to get and store a `Foo`:

```ts
// FooRepository.ts  
export interface FooRepository {  
  getById: (id: string) => RTE<unknown, Error, Foo>;  
  store: (foo: Foo) => RTE<unknown, Error, void>;  
}  
  
export interface FooRepositoryAccess {  
  fooRepository: FooRepository;  
}  
export declare const FooRepository: {  
  getById: (id: string) => RTE<FooRepositoryAccess, Error, Foo>;  
  store: (foo: Foo) => RTE<FooRepositoryAccess, Error, void>;  
};  
export declare const makeFooRepository: () => Promise<FooRepository>;
```

Note:

*   We follow the [module pattern](https://degoes.net/articles/zio-environment#the-module-pattern) when defining the `FooRepositoryAccess` interface to enable context aggregation when composing multiple `ReaderTaskEither`:

```ts
declare const a: RTE<{ serviceA: ServiceA },never,void>  
declare const b: RTE<{ serviceB: ServiceB },never,void>  
const ab: RTE<{ serviceA: ServiceA; serviceB: ServiceB },never,void>   
    = rte.flatMap(a,() => b)
```

*   We define a [companion object](https://stefan-bauer.online/blog/posts/writing-better-type-script#know-and-use-the-companion-object-pattern) `FooRepository` that exposes the same methods as the repository itself, except that they each require a context with `FooRepositoryAccess`. This makes for more concise code later on:

```ts
const theLongWay: RTE<FooRepositoryAccess, Error, Foo> = pipe(  
 rte.ask<FooRepositoryAccess>(),  
 rte.flatMap(({ fooRepository }) => fooRepository.getById('id'))  
);  
  
const theEasyWay: RTE<FooRepositoryAccess, Error, Foo>   
 = FooRepository.getById('id')
```

We also define a service port to transform a `Foo`:

```ts
// TransformFooService.ts  
export interface TransformFooService {  
  transform: (foo: Foo) => RTE<unknown, Error, Foo>;  
}  
  
export interface TransformFooServiceAccess {  
  transformFooService: TransformFooService;  
}  
  
export declare const TransformFooService: {  
  transform: (foo: Foo) => RTE<TransformFooServiceAccess, Error, Foo>;  
};  
  
declare const makeTransformFooService: () => Promise<TransformFooService>;
```

Next we can write two use cases: one to create a new `Foo` , and another one to transform a `Foo`:

```ts
// usecases.ts  
export const createFooUseCase = (id:string) =>   
 pipe(  
   rte.of(Foo.make(id)),  
   rte.tap(FooRepository.store)  
 );  
  
export const transformFooUseCase = (id: string) =>  
  pipe(  
    FooRepository.getById(id),  
    rte.flatMap(TransformFooService.transform),  
    rte.flatMap(FooRepository.store)  
  );
```

Finally, we can write our `main` that will create all the port adapters and invoke our use cases:

```ts
// index.ts  
const main = async () => {  
  const fooRepository = await makeFooRepository();  
  const transformFooService = await makeTransformFooService();  
  await createFooUseCase("my-foo-id")({  
    transformFooService,  
    fooRepository,  
  })();  
  await transformFooUseCase("my-foo-id")({  
    transformFooService,  
    fooRepository,  
  })();  
};  
main();
```

Create Effect proxies of the ports
----------------------------------

This step consists in generating new companion objects `FooRepository` and `TransformFooService` for our ports that are exposing an Effect version of the member methods.

First we rename the companion objects, adding a `Fpts` suffix:

```ts
// FooRepository.ts  
e̶x̶p̶o̶r̶t̶ ̶d̶e̶c̶l̶a̶r̶e̶ ̶c̶o̶n̶s̶t̶ ̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶:̶ ̶{̶  
export declare const FooRepositoryFpts: {  
  getById: (id: string) => RTE<FooRepositoryAccess, Error, Foo>;  
  store: (foo: Foo) => RTE<FooRepositoryAccess, Error, void>;  
};  
  
// TransformFooService.ts  
e̶x̶p̶o̶r̶t̶ ̶d̶e̶c̶l̶a̶r̶e̶ ̶c̶o̶n̶s̶t̶ ̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶:̶ ̶{̶  
export declare const TransformFooServiceFpts: {  
  transform: (foo: Foo) => RTE<TransformFooServiceAccess, Error, Foo>;  
};  
  
// usecases.ts  
export const createFooUseCase = (id:string) =>   
 pipe(  
   rte.of(Foo.make(id)),  
   r̶t̶e̶.̶t̶a̶p̶(̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶.̶s̶t̶o̶r̶e̶)̶  
   rte.tap(FooRepositoryFpts.store)  
 );  
  
export const transformFooUseCase = (id: string) =>  
  pipe(  
    F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶.̶g̶e̶t̶B̶y̶I̶d̶(̶i̶d̶)̶,̶  
    r̶t̶e̶.̶f̶l̶a̶t̶M̶a̶p̶(̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶.̶t̶r̶a̶n̶s̶f̶o̶r̶m̶)̶,̶  
    r̶t̶e̶.̶f̶l̶a̶t̶M̶a̶p̶(̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶.̶s̶t̶o̶r̶e̶)̶  
    FooRepositoryFpts.getById(id),  
    rte.flatMap(TransformFooServiceFpts.transform),  
    rte.flatMap(FooRepositoryFpts.store)  
  );
```

Then we use the `portToEffect` helper function to generate the Effect companion objects from the previous companion objects:

```ts
// FooRepository.ts  
export const FooRepositoryTag = Context.GenericTag<FooRepository>(  
 "FooRepository"  
);  
export const FooRepository = portToEffect(FooRepositoryFpts, {  
  fooRepository: FooRepositoryTag,  
}); // { getById: (id: string) => Effect<Foo, Error, FooRepository> ... }  
  
// TransformFooService.ts  
export const TransformFooServiceTag = Context.GenericTag<TransformFooService>(  
  "TransformFooService"  
);  
export const TransformFooService = portToEffect(TransformFooServiceFpts, {  
  transformFooService: TransformFooServiceTag,  
}); // { transform: (foo: Foo) => Effect<Foo, Error, TransformFooService> }
```

Rewrite a use case in Effect
----------------------------

At this point we can start using our newly generated Effect companion objects to rewrite the `transformFooUseCase` use case in Effect. Note that we voluntarily leave the `createFooUseCase` use case as is to simulate a migration that is ongoing, as opposed to a “big-bang” migration where we would convert all of our use cases to Effect in one go (much harder and riskier).

```ts
// usecases.ts  
export const transformFooUseCase = (id: string) =>  
  pipe(  
    F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶F̶p̶t̶s̶.̶g̶e̶t̶B̶y̶I̶d̶(̶i̶d̶)̶,̶  
    r̶t̶e̶.̶f̶l̶a̶t̶M̶a̶p̶(̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶F̶p̶t̶s̶.̶t̶r̶a̶n̶s̶f̶o̶r̶m̶)̶,̶  
    r̶t̶e̶.̶f̶l̶a̶t̶M̶a̶p̶(̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶F̶p̶t̶s̶.̶s̶t̶o̶r̶e̶)̶  
    FooRepository.getById(id),  
    Effect.flatMap(TransformFooService.transform),  
    Effect.flatMap(FooRepository.store)  
  ); // Effect<void, Error, TransformFooService | FooRepository>
```

Since we don’t want to impact our `main` program yet, we must maintain an fp-ts version of this use case, for backward compatibility. We can generate it from the Effect version thanks to the `functionToFpts` helper function:

```ts
// usecases.ts  
export const transformFooUseCaseFpts = functionToFpts(transformFooUseCase, {  
  fooRepository: FooRepositoryTag,  
  transformFooService: TransformFooServiceTag,  
}); // RTE<TransformFooServiceAccess & FooRepositoryAccess, Error, void>  
  
// index.ts  
const main = async () => {  
  const fooRepository = await makeFooRepository();  
  const transformFooService = await makeTransformFooService();  
  await createFooUseCase("my-foo-id")({  
   transformFooService,  
    fooRepository,  
  })();  
  a̶w̶a̶i̶t̶ ̶t̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶U̶s̶e̶C̶a̶s̶e̶(̶"̶m̶y̶-̶f̶o̶o̶-̶i̶d̶"̶)̶(̶{̶  
  await transformFooUseCaseFpts("my-foo-id")({  
    transformFooService,  
    fooRepository,  
  })();  
};  
main();
```

Convert ports to Effect
-----------------------

Next we convert our `FooRepository` port to Effect directly:

```ts
// FooRepository.ts  
export interface FooRepository {  
  g̶e̶t̶B̶y̶I̶d̶:̶ ̶(̶i̶d̶:̶ ̶s̶t̶r̶i̶n̶g̶)̶ ̶=̶>̶ ̶R̶T̶E̶<̶u̶n̶k̶n̶o̶w̶n̶,̶ ̶E̶r̶r̶o̶r̶,̶ ̶F̶o̶o̶>̶;̶  
  s̶t̶o̶r̶e̶:̶ ̶(̶f̶o̶o̶:̶ ̶F̶o̶o̶)̶ ̶=̶>̶ ̶R̶T̶E̶<̶u̶n̶k̶n̶o̶w̶n̶,̶ ̶E̶r̶r̶o̶r̶,̶ ̶v̶o̶i̶d̶>̶;̶  
  getById: (id: string) => Effect.Effect<Foo, Error>;  
  store: (foo: Foo) => Effect.Effect<void, Error>;  
}
```

We can now generate the Effect companion object using `Effect.serviceFunctions`:

```ts
// FooRepository.ts  
e̶x̶p̶o̶r̶t̶ ̶c̶o̶n̶s̶t̶ ̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶ ̶=̶ ̶p̶o̶r̶t̶T̶o̶E̶f̶f̶e̶c̶t̶(̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶F̶p̶t̶s̶,̶ ̶{̶  
̶ ̶ ̶f̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶:̶ ̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶T̶a̶g̶,̶  
̶}̶)̶;̶  
export const FooRepository = Effect.serviceFunctions(FooRepositoryTag);
```

Finally, for backward compatibility, we must maintain the fp-ts companion object. We can generate it using the `portToFpts` helper function:

```ts
// FooRepository.ts  
e̶x̶p̶o̶r̶t̶ ̶d̶e̶c̶l̶a̶r̶e̶ ̶c̶o̶n̶s̶t̶ ̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶F̶p̶t̶s̶:̶ ̶{̶  
̶ ̶ ̶g̶e̶t̶B̶y̶I̶d̶:̶ ̶(̶i̶d̶:̶ ̶s̶t̶r̶i̶n̶g̶)̶ ̶=̶>̶ ̶R̶T̶E̶<̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶A̶c̶c̶e̶s̶s̶,̶ ̶E̶r̶r̶o̶r̶,̶ ̶F̶o̶o̶>̶;̶  
̶ ̶ ̶s̶t̶o̶r̶e̶:̶ ̶(̶f̶o̶o̶:̶ ̶F̶o̶o̶)̶ ̶=̶>̶ ̶R̶T̶E̶<̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶A̶c̶c̶e̶s̶s̶,̶ ̶E̶r̶r̶o̶r̶,̶ ̶v̶o̶i̶d̶>̶;̶  
̶}̶;̶  
  
export const FooRepositoryFpts = portToFpts(FooRepository, {  
  fooRepository: FooRepositoryTag,  
}); // { getById: (id: string) => RTE<FooRepositoryAccess, Error, Foo>; ... }
```

We do the same for the `TransformFooService` port:

```ts
// TransformFooService.ts  
export interface TransformFooService {  
  t̶r̶a̶n̶s̶f̶o̶r̶m̶:̶ ̶(̶f̶o̶o̶:̶ ̶F̶o̶o̶)̶ ̶=̶>̶ ̶R̶T̶E̶<̶u̶n̶k̶n̶o̶w̶n̶,̶ ̶E̶r̶r̶o̶r̶,̶ ̶F̶o̶o̶>̶;̶  
  transform: (foo: Foo) => Effect<Foo, Error>;  
}  
  
e̶x̶p̶o̶r̶t̶ ̶c̶o̶n̶s̶t̶ ̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶ ̶=̶ ̶p̶o̶r̶t̶T̶o̶E̶f̶f̶e̶c̶t̶(̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶F̶p̶t̶s̶,̶ ̶{̶  
̶ ̶ ̶t̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶:̶ ̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶T̶a̶g̶,̶  
̶}̶)̶;̶  
export const TransformFooService = Effect.serviceFunctions(  
 TransformFooServiceTag  
);  
  
e̶x̶p̶o̶r̶t̶ ̶d̶e̶c̶l̶a̶r̶e̶ ̶c̶o̶n̶s̶t̶ ̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶F̶p̶t̶s̶:̶ ̶{̶  
̶ ̶ ̶t̶r̶a̶n̶s̶f̶o̶r̶m̶:̶ ̶(̶f̶o̶o̶:̶ ̶F̶o̶o̶)̶ ̶=̶>̶ ̶R̶T̶E̶<̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶A̶c̶c̶e̶s̶s̶,̶ ̶E̶r̶r̶o̶r̶,̶ ̶F̶o̶o̶>̶;̶  
̶}̶;̶  
export const TransformFooServiceFpts = portToFpts(TransformFooService, {  
  fooRepository: FooRepositoryTag,  
}); // { transform: (foo: Foo) => RTE<unknown, Error, Foo>; }
```

Note that we have not changed our `main` in this step and it can still be run without a problem.

Use ManagedRuntime to run Effect usecases
-----------------------------------------

In order to run the `transformFooUseCase` as an Effect, we must be able to provide our ports via Layers:

```ts
// FooRepository.ts  
e̶x̶p̶o̶r̶t̶ ̶d̶e̶c̶l̶a̶r̶e̶ ̶c̶o̶n̶s̶t̶ ̶m̶a̶k̶e̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶:̶ ̶(̶)̶ ̶=̶>̶ ̶P̶r̶o̶m̶i̶s̶e̶<̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶>̶;̶  
export declare const FooRepositoryLive: Layer.Layer<FooRepository>;  
  
// TransformFooService.ts  
d̶e̶c̶l̶a̶r̶e̶ ̶c̶o̶n̶s̶t̶ ̶m̶a̶k̶e̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶:̶ ̶(̶)̶ ̶=̶>̶ ̶P̶r̶o̶m̶i̶s̶e̶<̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶>̶;̶  
declare const TransformFooServiceLive: Layer.Layer<TransformFooService>;
```

Next we can create a `ManagedRuntime` and extract all the ports from the runtime context using the `contextToFpts` helper:

```ts
// index.ts  
const main = async () => {  
  c̶o̶n̶s̶t̶ ̶f̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶ ̶=̶ ̶a̶w̶a̶i̶t̶ ̶m̶a̶k̶e̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶(̶)̶;̶  
  c̶o̶n̶s̶t̶ ̶t̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶ ̶=̶ ̶a̶w̶a̶i̶t̶ ̶m̶a̶k̶e̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶(̶)̶;̶  
  const runtime = ManagedRuntime.make(  
    Layer.mergeAll(FooRepositoryLive, TransformFooServiceLive)  
  );  
  const { context } = await runtime.runtime();  
  const { fooRepository, transformFooService } = contextToFpts(context, {  
    fooRepository: FooRepositoryTag,  
    transformFooService: TransformFooServiceTag,  
  });  
  await createFooUseCase("my-foo-id")({  
   transformFooService,  
    fooRepository,  
  })();  
  await transformFooUseCaseFpts("my-foo-id")({  
    transformFooService,  
    fooRepository,  
  })();  
};  
main();
```

Finally, we can use the runtime to run the Effect `transformFooUseCase`:

```ts
// index.ts  
const main = async () => {  
  const runtime = ManagedRuntime.make(  
    Layer.mergeAll(FooRepositoryLive, TransformFooServiceLive)  
  );  
  const { context } = await runtime.runtime();  
  const { fooRepository, transformFooService } = contextToFpts(context, {  
    fooRepository: FooRepositoryTag,  
    transformFooService: TransformFooServiceTag,  
  });  
  await createFooUseCase("my-foo-id")({  
   transformFooService,  
    fooRepository,  
  })();  
  a̶w̶a̶i̶t̶ ̶t̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶U̶s̶e̶C̶a̶s̶e̶F̶p̶t̶s̶(̶"̶m̶y̶-̶f̶o̶o̶-̶i̶d̶"̶)̶(̶{̶
   ̶t̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶,̶  
   ̶f̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶,̶  
  }̶)̶(̶)̶;̶  
 await runtime.runPromise(transformFooUseCase("my-foo-id"));
};  
main();
```

Note that, once again, we left the `createFooUseCase` use case as is to show that we can be in a hybrid state where only part of the use cases have been migrated to Effect.

Bonus: simplify fp-ts ↔ effect tag mapping management
-----------------------------------------------------

All of the helpers we have used throughout this migration require a mapping object to go from the key name of the fp-ts port Access interface (eg `transformFooService` of `TransformFooServiceAccess`) to the `Tag` of the corresponding Effect port. For example:

```ts
contextToFpts(context, {  
  fooRepository: FooRepositoryTag,  
  transformFooService: TransformFooServiceTag,  
});
```

This mapping is essential for all the helpers to work correctly. It is not ideal to have to craft them like that all the time. To help us with that, we introduce:

```ts
const FptsConvertibleId = Symbol();  
interface FptsConvertible<T extends string> {   
  [FptsConvertibleId]: T;  
}
```

We can now embed this conversion information at the type level of our ports:

```ts
// FooRepository.ts  
export interface FooRepository extends FptsConvertible<"fooRepository"> {  
  getById: (id: string) => Effect.Effect<Foo, Error>;  
  store: (foo: Foo) => Effect.Effect<void, Error>;  
}  
  
// TransformFooService.ts  
export interface TransformFooService   
  extends FptsConvertible<"transformFooService"> {  
  transform: (foo: Foo) => Effect<Foo, Error>;  
}
```

The first thing we can do with this is to simplify the definition of Access interfaces using a type helper `FptsAccess`:

```ts
// FooRepository.ts  
export interface FooRepositoryAccess extends FptsAccess<FooRepository> {}  
  
// TransformFooService.ts  
export interface TransformFooServiceAccess   
  extends FptsAccess<TransformFooService> {}
```

And we can also define smaller atomic mapping objects using a new helper `getFptsMapping`:

```ts
// FooRepository.ts  
const FooRepositoryFptsMapping = getFptsMapping(  
  FooRepositoryTag,  
  "fooRepository"  
); // { fooRepository: FooRepositoryTag }  
  
// TransformFooService.ts  
const TransformFooServiceFptsMapping = getFptsMapping(  
  TransformFooServiceTag,  
  "transformFooService"  
); // { transformFooService: TransformFooServiceTag }
```

Note: It looks like we are once again typing the key `"fooRepository"` or `"transformFooService"` but in fact, the function `getFptsMapping` is type-safe so that given `FooRepositoryTag` as first argument, only the string `"fooRepository"` is valid as second argument. So your code editor will autocomplete it for you. Moreover, the compiler will break if you change the definition in the `FptsConvertible` so it is not really an additional burden.

We can now combine these two mapping objects when calling `contextToFpts` or any other helper:

```ts
contextToFpts(context, {  
  f̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶:̶ ̶F̶o̶o̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶T̶a̶g̶,̶  
  t̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶:̶ ̶T̶r̶a̶n̶s̶f̶o̶r̶m̶F̶o̶o̶S̶e̶r̶v̶i̶c̶e̶T̶a̶g̶,̶  
  ...FooRepositoryFptsMapping,  
  ...TransformFooServiceFptsMapping,  
});
```

## Conclusion

Our objective of being able to write any new use case or port using Effect was accomplished in 2 months (working around 10% of our time on it)!

Teamwork was definitely a big part of this success: first, we have to mention [Stephane Ledorze](https://medium.com/u/c67fddd7ca1c?source=post_page-----b71acd0c5640--------------------------------) as he migrated all our repositories single-handedly and gave us great advice on how to define our migration strategy. We handled the rest with the whole team during dedicated “tech sessions” that we do every Wednesday afternoon at Inato: during those sessions, we stop delivering features to be able to focus on purely tech subjects, which was a great occasion to migrate the many ports we had to handle and onboard the team on Effect.

As we’re writing this article, we have around 150 full Effect use cases. The rest of the existing use cases will be migrated on the go whenever we need to update them!

We’re already seeing great improvements: for example, implementing rate limiting with just a few lines of code with Effect, whereas we needed a big amount of code to do it with fp-ts. We’re eager to leverage even more the Effect ecosystem now that we have officially migrated to it!

We hope this article motivated you to take the leap from fp-ts to Effect, don’t hesitate to comment if you have any questions or comments!

_This article was written by_ [_Jeremie Dayan_](https://medium.com/u/32aa24b2222c?source=post_page-----b71acd0c5640--------------------------------) _and_ [_Laure Retru-Chavastel_](https://medium.com/u/8fb677159f4f?source=post_page-----b71acd0c5640--------------------------------)