Skip to main content

Clean Architecture with Archer

Understanding how Archer simplifies Clean Architecture implementation.

Traditional Clean Architecture Flow​

In a typical Clean Architecture implementation, data flows through multiple layers with extensive boilerplate:

graph LR

UC["UseCase"]
Repo["Repository"]
DSL(("DataSource Local"))
DSR(("DataSource Remote"))
MRTD{{"Map to Domain"}}
MDTD{{"Map to DBO"}}
MDBTD{{"Map Domain"}}

UC -->|"1: Request data"| Repo
Repo -->|"2: Fetch remote"| DSR
DSR -->|"3: Return data"| MRTD
MRTD -->|"3a: Map to Domain"| Repo
Repo -->|"4: Store local"| MDTD
MDTD -->|"4a: Map to DBO"| DSL
DSL -->|"5: Return data"| MDBTD
MDBTD -->|"5a: Map to Domain"| Repo
Repo -->|"6: Return data"| UC

classDef mapping fill:#b44,stroke:#333,stroke-width:2px;
class MRTD,MDTD,MDBTD mapping;

Legend​

  • UC - UseCase
  • DSL - DataSource Local
  • DSR - DataSource Remote

Traditional Implementation Requirements​

For the above diagram, you typically need:

  1. UseCase Layer

    • Interface: GetDataUseCase
    • Implementation: GetDataUseCaseImpl
  2. Repository Layer

    • Interface: DataRepository
    • Implementation: DataRepositoryImpl
  3. DataSource Layer (×2 for network and local)

    • Interface: RemoteDataSource
    • Implementation: RemoteDataSourceImpl
    • Interface: LocalDataSource
    • Implementation: LocalDataSourceImpl
  4. Mapping Layer

    • Network model → Domain model mapper
    • Domain model → Local model mapper
    • Local model → Domain model mapper

Total: ~11 files for a single data flow!

Archer's Approach​

Archer reduces this to just the essential parts:

// 1. Define data sources (no interfaces needed!)
val remoteDataSource = getDataSource<UserId, User> { userId ->
api.fetchUser(userId).toDomain() // You still need mapping
}

val localDataSource: StoreDataSource<UserId, User> =
DatabaseDataSource() // Implement get/put operations

// 2. Create repository strategy
val userRepository = remoteDataSource cacheWith localDataSource expiresIn 5.minutes

// 3. Use it directly
val user = either {
userRepository.get(StoreSync, userId)
}

What Archer Provides​

  1. Contractual DataSources - Predefined contracts that eliminate interface boilerplate
  2. Composable Repositories - Build complex data strategies with simple DSL
  3. Multiple Result Types - Ice, Either, or Nullable based on your needs
  4. Built-in Patterns - Caching, validation, error handling

What You Still Implement​

  • Mapping Logic - Data transformation between layers (this is essential!)
  • Business Logic - Your actual use cases
  • Data Source Implementation - How you fetch/store data

Benefits​

Abstraction​

Clean separation between data fetching, caching, and business logic.

Reusability​

DataSources and repositories are highly composable and reusable.

Scalability​

Easy to add new data sources or change caching strategies.

Less Boilerplate​

No need to create interfaces for every layer.

Next Steps​