OBP-API/LIFT_HTTP4S_COEXISTENCE.md

974 lines
26 KiB
Markdown
Raw Normal View History

2025-12-03 07:09:36 +00:00
# Lift and http4s Coexistence Strategy
## Question
Can http4s and Lift coexist in the same project to convert endpoints one by one?
## Answer: Yes, on Different Ports
2025-12-05 07:14:45 +00:00
## OBP-API-Dispatch
OBP-API-Dispatch already exists:
- **Location:** `workspace_2024/OBP-API-Dispatch`
- **GitHub:** https://github.com/OpenBankProject/OBP-API-Dispatch
- **Technology:** http4s (Cats Effect 3, Ember server)
- **Purpose:** Routes requests between different OBP-API backends
- **Current routing:** Based on API version (v1.3.0 → backend 2, others → backend 1)
It needs minor enhancements to support version-based routing for the Lift → http4s migration.
## Answers to Your Three Questions
### Q1: Could we use OBP-API-Dispatch to route between two ports?
**YES - it already exists**
OBP-API-Dispatch can be used for this:
- Single entry point for clients
2025-12-05 07:24:02 +00:00
- Route by API version: v4/v5 → Lift, v6/v7 → http4s (might want to route based on resource docs but not sure if we really need this - NGINX might therefore be an alternative but OBP-Dispatch would have OBP specific routing out of the box and potentially other features)
2025-12-05 07:14:45 +00:00
- No client configuration changes needed
- Rollback by changing routing config
### Q2: Running http4s in Jetty until migration complete?
**Possible but not recommended**
Running http4s in Jetty (servlet mode) loses:
- True non-blocking I/O
- HTTP/2, WebSockets, efficient streaming
- Would need to refactor again later to standalone
Use standalone http4s on port 8081 from the start.
### Q3: How would the developer experience be?
**IDE and Project Setup:**
You'll work in **one IDE window** with the OBP-API codebase:
- Same project structure
- Same database (Lift Boot continues to handle DB creation/migrations)
- Both Lift and http4s code in the same `obp-api` module
- Edit both Lift endpoints and http4s endpoints in the same IDE
**Running the Servers:**
You'll run **three separate terminal processes**:
**Terminal 1: Lift Server (existing)**
```bash
cd workspace_2024/OBP-API-C/OBP-API
sbt "project obp-api" run
```
- Runs Lift Boot (handles DB initialization)
- Starts on port 8080
- Keep this running as long as you have Lift endpoints
**Terminal 2: http4s Server (new)**
```bash
cd workspace_2024/OBP-API-C/OBP-API
sbt "project obp-api" "runMain code.api.http4s.Http4sMain"
```
- Starts on port 8081
- Separate process from Lift
- Uses same database connection pool
**Terminal 3: OBP-API-Dispatch (separate project)**
```bash
cd workspace_2024/OBP-API-Dispatch
mvn clean package
java -jar target/OBP-API-Dispatch-1.0-SNAPSHOT-jar-with-dependencies.jar
```
- Separate IDE window or just a terminal
- Routes requests between Lift (8080) and http4s (8081)
- Runs on port 8088
**Editing Workflow:**
1. **Adding new http4s endpoint:**
- Create endpoint in `obp-api/src/main/scala/code/api/http4s/`
- Edit in same IDE as Lift code
- Restart Terminal 2 only (http4s server)
2. **Fixing Lift endpoint:**
- Edit existing Lift code in `obp-api/src/main/scala/code/api/`
- Restart Terminal 1 only (Lift server)
3. **Updating routing (which endpoints go where):**
- Edit `OBP-API-Dispatch/src/main/resources/application.conf`
- Restart Terminal 3 only (Dispatch)
**Database:**
Lift Boot continues to handle:
- Database connection setup
- Schema migrations
- Table creation
Both Lift and http4s use the same database connection pool and Mapper classes.
### Architecture with OBP-API-Dispatch
```
┌────────────────────────────────────────────┐
│ API Clients │
└────────────────────────────────────────────┘
Port 8088/443
┌────────────────────────────────────────────┐
│ OBP-API-Dispatch (http4s) │
│ │
│ Routing Rules: │
│ • /obp/v4.0.0/* → Lift (8080) │
│ • /obp/v5.0.0/* → Lift (8080) │
│ • /obp/v5.1.0/* → Lift (8080) │
│ • /obp/v6.0.0/* → http4s (8081) ✨ │
│ • /obp/v7.0.0/* → http4s (8081) ✨ │
└────────────────────────────────────────────┘
↓ ↓
┌─────────┐ ┌─────────┐
│ Lift │ │ http4s │
│ :8080 │ │ :8081 │
└─────────┘ └─────────┘
↓ ↓
┌────────────────────────────────┐
│ Shared Resources: │
│ - Database │
│ - Business Logic │
│ - Authentication │
│ - ResourceDocs │
└────────────────────────────────┘
2025-12-03 07:09:36 +00:00
```
### How It Works
1. **Two HTTP Servers Running Simultaneously**
- Lift/Jetty continues on port 8080
- http4s starts on port 8081
- Both run in the same JVM process
2. **Shared Components**
- Database connections
- Business logic layer
- Authentication/authorization
- Configuration
- Connector layer
3. **Gradual Migration**
- Start: All endpoints on Lift (port 8080)
- During: Some on Lift, some on http4s
- End: All on http4s (port 8081), Lift removed
2025-12-05 07:14:45 +00:00
## Using OBP-API-Dispatch for Routing
### Current Status
OBP-API-Dispatch already exists and is functional:
- **Location:** `workspace_2024/OBP-API-Dispatch`
- **GitHub:** https://github.com/OpenBankProject/OBP-API-Dispatch
- **Build:** Maven-based
- **Technology:** http4s with Ember server
- **Current routing:** Routes v1.3.0 to backend 2, others to backend 1
### Current Configuration
```hocon
# application.conf
app {
dispatch_host = "127.0.0.1"
dispatch_dev_port = 8088
obp_api_1_base_uri = "http://localhost:8080"
obp_api_2_base_uri = "http://localhost:8086"
}
```
### Enhancement for Migration
Update configuration to support version-based routing:
```hocon
# application.conf
app {
dispatch_host = "0.0.0.0"
dispatch_port = 8088
# Lift backend (legacy endpoints)
lift_backend_uri = "http://localhost:8080"
lift_backend_uri = ${?LIFT_BACKEND_URI}
# http4s backend (modern endpoints)
http4s_backend_uri = "http://localhost:8081"
http4s_backend_uri = ${?HTTP4S_BACKEND_URI}
# Routing strategy
routing {
# API versions that go to http4s backend
http4s_versions = ["v6.0.0", "v7.0.0"]
# Specific endpoint overrides (optional)
overrides = [
# Example: migrate specific v5.1.0 endpoints early
# { path = "/obp/v5.1.0/banks", target = "http4s" }
]
}
}
```
### Enhanced Routing Logic
Update `ObpApiDispatch.scala` to support version-based routing:
```scala
private def selectBackend(path: String, method: Method): String = {
// Extract API version from path: /obp/v{version}/...
val versionPattern = """/obp/(v\d+\.\d+\.\d+)/.*""".r
path match {
case versionPattern(version) =>
if (http4sVersions.contains(version)) {
logger.info(s"Version $version routed to http4s")
"http4s"
} else {
logger.info(s"Version $version routed to Lift")
"lift"
}
case _ =>
logger.debug(s"Path $path routing to Lift (default)")
"lift"
}
}
```
### Local Development Workflow
**Terminal 1: Start Lift**
```bash
cd workspace_2024/OBP-API-C/OBP-API
sbt "project obp-api" run
# Starts on port 8080
```
**Terminal 2: Start http4s**
```bash
cd workspace_2024/OBP-API-C/OBP-API
sbt "project obp-api" "runMain code.api.http4s.Http4sMain"
# Starts on port 8081
```
**Terminal 3: Start OBP-API-Dispatch**
```bash
cd workspace_2024/OBP-API-Dispatch
mvn clean package
java -jar target/OBP-API-Dispatch-1.0-SNAPSHOT-jar-with-dependencies.jar
# Starts on port 8088
```
**Terminal 4: Test**
```bash
# Old endpoint (goes to Lift)
curl http://localhost:8088/obp/v5.1.0/banks
# New endpoint (goes to http4s)
curl http://localhost:8088/obp/v6.0.0/banks
# Health checks
curl http://localhost:8088/health
```
2025-12-03 07:09:36 +00:00
## Implementation Approach
### Step 1: Add http4s to Project
```scala
// build.sbt
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-dsl" % "0.23.x",
"org.http4s" %% "http4s-ember-server" % "0.23.x",
"org.http4s" %% "http4s-ember-client" % "0.23.x",
"org.http4s" %% "http4s-circe" % "0.23.x"
)
```
### Step 2: Create http4s Server
```scala
// code/api/http4s/Http4sServer.scala
package code.api.http4s
import cats.effect._
import org.http4s.ember.server.EmberServerBuilder
import com.comcast.ip4s._
object Http4sServer {
def start(port: Int = 8081): IO[Unit] = {
EmberServerBuilder
.default[IO]
.withHost(ipv4"0.0.0.0")
.withPort(Port.fromInt(port).get)
.withHttpApp(routes.orNotFound)
.build
.useForever
}
def routes = ??? // Define routes here
}
```
### Step 3: THE JETTY PROBLEM
2025-12-05 07:14:45 +00:00
If you start http4s from Lift's Bootstrap, it runs INSIDE Jetty's servlet container. This defeats the purpose of using http4s.
2025-12-03 07:09:36 +00:00
```
❌ WRONG APPROACH:
┌─────────────────────────────┐
│ Jetty Servlet Container │
│ ├─ Lift (port 8080) │
2025-12-05 07:14:45 +00:00
│ └─ http4s (port 8081) │ ← Still requires Jetty
2025-12-03 07:09:36 +00:00
└─────────────────────────────┘
```
**The Problem:**
- http4s would still require Jetty to run
- Can't remove servlet container later
- Defeats the goal of eliminating Jetty
### Step 3: CORRECT APPROACH - Separate Main
**Solution:** Start http4s as a STANDALONE server, NOT from Lift Bootstrap.
```
✓ CORRECT APPROACH:
┌──────────────────┐ ┌─────────────────┐
│ Jetty Container │ │ http4s Server │
│ └─ Lift │ │ (standalone) │
│ Port 8080 │ │ Port 8081 │
└──────────────────┘ └─────────────────┘
Same JVM Process
```
#### Option A: Two Separate Processes (Simpler)
```scala
// Run Lift as usual
sbt "jetty:start" // Port 8080
// Run http4s separately
sbt "runMain code.api.http4s.Http4sMain" // Port 8081
```
**Deployment:**
```bash
# Start Lift/Jetty
java -jar obp-api-jetty.jar &
# Start http4s standalone
java -jar obp-api-http4s.jar &
```
**Pros:**
- Complete separation
- Easy to understand
- Can stop/restart independently
- No Jetty dependency for http4s
**Cons:**
- Two separate processes to manage
- Two JVMs (more memory)
2025-12-05 07:24:02 +00:00
### Two Separate Processes
2025-12-03 07:09:36 +00:00
2025-12-05 07:14:45 +00:00
For actual migration:
2025-12-03 07:09:36 +00:00
1. **Keep Lift/Jetty running as-is** on port 8080
2. **Create standalone http4s server** on port 8081 with its own Main class
3. **Use reverse proxy** (nginx/HAProxy) to route requests
4. **Migrate endpoints one by one** to http4s
5. **Eventually remove Lift/Jetty** completely
```
Phase 1-3 (Migration):
┌─────────────┐
│ Nginx │ Port 443
│ (Proxy) │
└──────┬──────┘
├──→ Jetty/Lift (Process 1) Port 8080 ← Old endpoints
└──→ http4s standalone (Process 2) Port 8081 ← New endpoints
Phase 4 (Complete):
┌─────────────┐
│ Nginx │ Port 443
│ (Proxy) │
└──────┬──────┘
└──→ http4s standalone Port 8080 ← All endpoints
2025-12-05 07:14:45 +00:00
(Jetty removed)
2025-12-03 07:09:36 +00:00
```
**This way http4s is NEVER dependent on Jetty.**
2025-12-03 21:39:09 +00:00
### Is http4s Non-Blocking?
**YES - http4s is fully non-blocking and asynchronous.**
#### Architecture Comparison
**Lift/Jetty (Blocking):**
```
Thread-per-request model:
┌─────────────────────────────┐
│ Request 1 → Thread 1 (busy) │ Blocks waiting for DB
│ Request 2 → Thread 2 (busy) │ Blocks waiting for HTTP call
│ Request 3 → Thread 3 (busy) │ Blocks waiting for file I/O
│ Request 4 → Thread 4 (busy) │
│ ... │
2025-12-05 07:14:45 +00:00
│ Request N → Thread pool full │ ← New requests wait
2025-12-03 21:39:09 +00:00
└─────────────────────────────┘
Problem:
- 1 thread per request
- Thread blocks on I/O
- Limited by thread pool size (e.g., 200 threads)
- More requests = more memory
```
**http4s (Non-Blocking):**
```
Async/Effect model:
┌─────────────────────────────┐
│ Thread 1: │
2025-12-05 07:14:45 +00:00
│ Request 1 → DB call (IO) │ ← Doesn't block - Fiber suspended
2025-12-03 21:39:09 +00:00
│ Request 2 → API call (IO) │ ← Continues processing
│ Request 3 → Processing │
│ Request N → ... │
└─────────────────────────────┘
Benefits:
- Few threads (typically = CPU cores)
- Thousands of concurrent requests
- Much lower memory usage
- Scales better
```
#### Performance Impact
**Lift/Jetty:**
- 200 threads × ~1MB stack = ~200MB just for threads
- Max ~200 concurrent blocking requests
- Each blocked thread = wasted resources
**http4s:**
- 8 threads (on 8-core machine) × ~1MB = ~8MB for threads
- Can handle 10,000+ concurrent requests
- Threads never block, always doing work
#### Code Example - Blocking vs Non-Blocking
**Lift (Blocking):**
```scala
// This BLOCKS the thread while waiting for DB
lazy val getBank: OBPEndpoint = {
case "banks" :: bankId :: Nil JsonGet _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
// Thread blocks here waiting for database
bank <- Future { Connector.connector.vend.getBank(BankId(bankId)) }
} yield {
(bank, HttpCode.`200`(cc))
}
}
}
// Under the hood:
// Thread 1: Wait for DB... (blocked, not doing anything else)
// Thread 2: Wait for DB... (blocked)
// Thread 3: Wait for DB... (blocked)
// Eventually: No threads left → requests queue up
```
**http4s (Non-Blocking):**
```scala
// This NEVER blocks - thread is freed while waiting
def getBank[F[_]: Concurrent](bankId: String): F[Response[F]] = {
for {
// Thread is released while waiting for DB
// Can handle other requests in the meantime
bank <- getBankFromDB(bankId) // Returns F[Bank], doesn't block
response <- Ok(bank.asJson)
} yield response
}
// Under the hood:
// Thread 1: Start DB call → release thread → handle other requests
// DB returns → pick up continuation → send response
// Same thread handles 100s of requests while others wait for I/O
```
#### Real-World Impact
**Scenario:** 1000 concurrent requests, each needs 100ms of DB time
**Lift/Jetty (200 thread pool):**
- First 200 requests: start immediately
- Requests 201-1000: wait in queue
- Total time: ~500ms (because of queuing)
- Memory: 200MB for threads
**http4s (8 threads):**
- All 1000 requests: start immediately
- Process concurrently on 8 threads
- Total time: ~100ms (no queuing)
- Memory: 8MB for threads
#### Why This Matters for Migration
1. **Better Resource Usage**
- Same machine can handle more requests
- Lower memory footprint
- Can scale vertically better
2. **No Thread Pool Tuning**
- Lift: Need to tune thread pool size (too small = slow, too large = OOM)
- http4s: Set to CPU cores, done
3. **Database Connections**
- Lift: Need thread pool ≤ DB connections (e.g., 200 threads = 200 DB connections)
- http4s: 8 threads can share smaller DB pool (e.g., 20 connections)
4. **Modern Architecture**
- http4s uses cats-effect (like Akka, ZIO, Monix)
- Industry standard for Scala backends
- Better ecosystem and tooling
#### The Blocking Problem in Current OBP-API
```scala
// Common pattern in OBP-API - BLOCKS thread
for {
bank <- Future { /* Get from DB - BLOCKS */ }
accounts <- Future { /* Get from DB - BLOCKS */ }
transactions <- Future { /* Get from Connector - BLOCKS */ }
} yield result
// Each Future ties up a thread waiting
// If 200 requests do this, 200 threads blocked
```
#### http4s Solution - Truly Async
```scala
// Non-blocking version
for {
bank <- IO { /* Get from DB - thread released */ }
accounts <- IO { /* Get from DB - thread released */ }
transactions <- IO { /* Get from Connector - thread released */ }
} yield result
// IO suspends computation, releases thread
// Thread can handle other work while waiting
// 8 threads can handle 1000s of these concurrently
```
### Conclusion on Non-Blocking
**Yes, http4s is non-blocking and this is a MAJOR reason to migrate:**
- Better performance (10-50x more concurrent requests)
- Lower memory usage
- Better resource utilization
- Scales much better
- Removes need for thread pool tuning
**However:** To get full benefits, you'll need to:
1. Use `IO` or `F[_]` instead of blocking `Future`
2. Use non-blocking database libraries (Doobie, Skunk)
3. Use non-blocking HTTP clients (http4s client)
But the migration can be gradual - even blocking code in http4s is still better than Lift/Jetty's servlet model.
2025-12-03 07:09:36 +00:00
### Step 4: Shared Business Logic
```scala
// Keep business logic separate from HTTP layer
package code.api.service
object UserService {
// Pure business logic - no Lift or http4s dependencies
def createUser(username: String, email: String, password: String): Box[User] = {
// Implementation
}
}
// Use in Lift endpoint
class LiftEndpoints extends RestHelper {
serve("obp" / "v6.0.0" prefix) {
case "users" :: Nil JsonPost json -> _ => {
val result = UserService.createUser(...)
// Return Lift response
}
}
}
// Use in http4s endpoint
class Http4sEndpoints[F[_]: Concurrent] {
def routes: HttpRoutes[F] = HttpRoutes.of[F] {
case req @ POST -> Root / "obp" / "v6.0.0" / "users" =>
val result = UserService.createUser(...)
// Return http4s response
}
}
```
## Migration Strategy
2025-12-05 07:14:45 +00:00
### Phase 1: Setup
2025-12-03 07:09:36 +00:00
- Add http4s dependencies
- Create http4s server infrastructure
- Start http4s on port 8081
- Keep all endpoints on Lift
2025-12-05 07:14:45 +00:00
### Phase 2: Convert New Endpoints
2025-12-03 07:09:36 +00:00
- All NEW endpoints go to http4s only
- Existing endpoints stay on Lift
- Share business logic between both
2025-12-05 07:14:45 +00:00
### Phase 3: Migrate Existing Endpoints
2025-12-03 07:09:36 +00:00
Priority order:
1. Simple GET endpoints (read-only, no sessions)
2. POST endpoints with simple authentication
3. Endpoints with complex authorization
4. Admin/management endpoints
5. OAuth/authentication endpoints (last)
2025-12-05 07:14:45 +00:00
### Phase 4: Deprecation
2025-12-03 07:09:36 +00:00
- Announce Lift endpoints deprecated
- Run both servers (port 8080 and 8081)
- Redirect/proxy 8080 -> 8081
- Update documentation
2025-12-05 07:14:45 +00:00
### Phase 5: Removal
2025-12-03 07:09:36 +00:00
- Remove Lift dependencies
- Remove Jetty dependency
- Single http4s server on port 8080
- No servlet container needed
## Request Routing During Migration
2025-12-05 07:14:45 +00:00
### OBP-API-Dispatch
2025-12-03 07:09:36 +00:00
```
2025-12-05 07:14:45 +00:00
Clients → OBP-API-Dispatch (8088/443)
├─→ Lift (8080) - v4, v5, v5.1
└─→ http4s (8081) - v6, v7
2025-12-03 07:09:36 +00:00
```
2025-12-05 07:14:45 +00:00
**OBP-API-Dispatch:**
- Already exists in workspace
- Already http4s-based
- Designed for routing between backends
- Has error handling, logging
- Needs routing logic updates
- Single entry point
- Route by version or endpoint
**Migration Phases:**
**Phase 1: Setup**
```hocon
routing {
http4s_versions = [] # All traffic to Lift
}
```
2025-12-03 07:09:36 +00:00
2025-12-05 07:14:45 +00:00
**Phase 2: First Migration**
```hocon
routing {
http4s_versions = ["v6.0.0"] # v6 to http4s
}
2025-12-03 07:09:36 +00:00
```
2025-12-05 07:14:45 +00:00
**Phase 3: Progressive**
```hocon
routing {
http4s_versions = ["v5.1.0", "v6.0.0", "v7.0.0"]
}
2025-12-03 07:09:36 +00:00
```
2025-12-05 07:14:45 +00:00
**Phase 4: Complete**
```hocon
routing {
http4s_versions = ["v4.0.0", "v5.0.0", "v5.1.0", "v6.0.0", "v7.0.0"]
}
```
2025-12-03 07:09:36 +00:00
2025-12-05 07:24:02 +00:00
### Alternative: Two Separate Ports
2025-12-03 07:09:36 +00:00
```
2025-12-05 07:14:45 +00:00
Clients → Load Balancer
├─→ Port 8080 (Lift)
└─→ Port 8081 (http4s)
2025-12-03 07:09:36 +00:00
```
2025-12-05 07:14:45 +00:00
Clients need to know which port to use
2025-12-03 07:09:36 +00:00
## Database Access Migration
### Current: Lift Mapper
```scala
class AuthUser extends MegaProtoUser[AuthUser] {
// Mapper ORM
}
```
### During Migration: Keep Mapper
```scala
// Both Lift and http4s use Mapper
// No need to migrate DB layer immediately
import code.model.dataAccess.AuthUser
// In http4s endpoint
def getUser(id: String): IO[Option[User]] = IO {
AuthUser.find(By(AuthUser.id, id)).map(_.toUser)
}
```
### Future: Replace Mapper (Optional)
```scala
// Use Doobie or Skunk
import doobie._
import doobie.implicits._
def getUser(id: String): ConnectionIO[Option[User]] = {
sql"SELECT * FROM authuser WHERE id = $id"
.query[User]
.option
}
```
## Configuration
```properties
# props/default.props
# Enable http4s server
http4s.enabled=true
http4s.port=8081
# Lift/Jetty (keep running)
jetty.port=8080
# Migration mode
# - "dual" = both servers running
# - "http4s-only" = only http4s
migration.mode=dual
```
## Testing Strategy
### Test Both Implementations
```scala
class UserEndpointTest extends ServerSetup {
// Test Lift version
scenario("Create user via Lift (port 8080)") {
val request = (v6_0_0_Request / "users").POST
val response = makePostRequest(request, userJson)
response.code should equal(201)
}
// Test http4s version
scenario("Create user via http4s (port 8081)") {
val request = (http4s_v6_0_0_Request / "users").POST
val response = makePostRequest(request, userJson)
response.code should equal(201)
}
// Test both give same result
scenario("Both implementations return same result") {
val liftResult = makeLiftRequest(...)
val http4sResult = makeHttp4sRequest(...)
liftResult should equal(http4sResult)
}
}
```
## Resource Docs Compatibility
### Keep Same ResourceDoc Structure
```scala
// Shared ResourceDoc definition
val createUserDoc = ResourceDoc(
createUser,
implementedInApiVersion,
"createUser",
"POST",
"/users",
"Create User",
"""Creates a new user...""",
postUserJson,
userResponseJson,
List(UserNotLoggedIn, InvalidJsonFormat)
)
// Lift endpoint references it
lazy val createUserLift: OBPEndpoint = {
case "users" :: Nil JsonPost json -> _ => {
// implementation
}
}
// http4s endpoint references same doc
def createUserHttp4s[F[_]: Concurrent]: HttpRoutes[F] = {
case req @ POST -> Root / "users" => {
// implementation
}
}
```
## Advantages of Coexistence Approach
1. **Zero Downtime Migration**
- Old endpoints keep working
- New endpoints added incrementally
- No big-bang rewrite
2. **Risk Mitigation**
- Test new framework alongside old
- Easy rollback per endpoint
- Gradual learning curve
3. **Business Continuity**
- No disruption to users
- Features can still be added
- Migration in background
4. **Shared Resources**
- Same database
- Same business logic
- Same configuration
2025-12-05 07:14:45 +00:00
2025-12-03 07:09:36 +00:00
## Challenges and Solutions
### Challenge 1: Port Management
**Solution:** Use property files to configure ports, allow override
### Challenge 2: Session/State Sharing
**Solution:** Use stateless JWT tokens, shared Redis for sessions
### Challenge 3: Authentication
**Solution:** Keep auth logic separate, callable from both frameworks
### Challenge 4: Database Connections
**Solution:** Shared connection pool, configure max connections appropriately
### Challenge 5: Monitoring
**Solution:** Separate metrics for each server, aggregate in monitoring system
### Challenge 6: Deployment
**Solution:** Single JAR with both servers, configure which to start
## Deployment Considerations
### Development
```bash
# Start with both servers
sbt run
# Lift on :8080, http4s on :8081
```
### Production - Transition Period
```
# Run both servers
java -Dhttp4s.enabled=true \
-Dhttp4s.port=8081 \
-Djetty.port=8080 \
-jar obp-api.jar
```
### Production - After Migration
```
# Only http4s
java -Dhttp4s.enabled=true \
-Dhttp4s.port=8080 \
-Dlift.enabled=false \
-jar obp-api.jar
```
## Example: First Endpoint Migration
### 1. Existing Lift Endpoint
```scala
// APIMethods600.scala
lazy val getBank: OBPEndpoint = {
case "banks" :: bankId :: Nil JsonGet _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
bank <- Future { Connector.connector.vend.getBank(BankId(bankId)) }
} yield {
(bank, HttpCode.`200`(cc))
}
}
}
```
### 2. Create http4s Version
```scala
// code/api/http4s/endpoints/BankEndpoints.scala
class BankEndpoints[F[_]: Concurrent] {
def routes: HttpRoutes[F] = HttpRoutes.of[F] {
case GET -> Root / "obp" / "v6.0.0" / "banks" / bankId =>
// Same business logic
val bankBox = Connector.connector.vend.getBank(BankId(bankId))
bankBox match {
case Full(bank) => Ok(bank.toJson)
case Empty => NotFound()
case Failure(msg, _, _) => BadRequest(msg)
}
}
}
```
### 3. Both Available
- Lift: `http://localhost:8080/obp/v6.0.0/banks/{bankId}`
- http4s: `http://localhost:8081/obp/v6.0.0/banks/{bankId}`
### 4. Test Both
```scala
scenario("Get bank - Lift version") {
val response = makeGetRequest(v6_0_0_Request / "banks" / testBankId.value)
response.code should equal(200)
}
scenario("Get bank - http4s version") {
val response = makeGetRequest(http4s_v6_0_0_Request / "banks" / testBankId.value)
response.code should equal(200)
}
```
### 5. Deprecate Lift Version
- Add deprecation warning to Lift endpoint
- Update docs to point to http4s version
- Monitor usage
### 6. Remove Lift Version
- Delete Lift endpoint code
- All traffic to http4s
## Conclusion
**Yes, Lift and http4s can coexist** by running on different ports (8080 and 8081) within the same application. This allows for:
- Gradual, low-risk migration
- Endpoint-by-endpoint conversion
- Shared business logic and resources
- Zero downtime
2025-12-05 07:14:45 +00:00
- Flexible migration pace
2025-12-03 07:09:36 +00:00
The key is to **keep HTTP layer separate from business logic** so both frameworks can call the same underlying functions.
2025-12-05 07:24:02 +00:00
Start with simple read-only endpoints, then gradually migrate more complex ones, finally removing Lift when all endpoints are converted.