Skip to main content

7 posts tagged with "Kotlin"

Posts about the Kotlin programming language and its ecosystem

View All Tags

Why Shank is Superior to Spring for Dependency Injection

· 8 min read
Snitch Team
Snitch Framework Maintainers

Dependency Injection (DI) has become a cornerstone of modern application development. For years, Spring has dominated this space with its rich ecosystem and comprehensive feature set. However, for Kotlin developers seeking a more streamlined, type-safe, and performant solution, Shank emerges as the clear superior alternative.

The Pain Points of Spring Dependency Injection

If you've worked with Spring for any length of time, you're likely familiar with this scenario:

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: 
No qualifying bean of type 'com.example.UserService' available:
expected at least 1 bean which qualifies as autowire candidate.

This cryptic runtime error appears only after your application has started—often in production—leaving you scrambling to determine why Spring couldn't find your dependency. Was it missing a @Component annotation? Perhaps you forgot to include a configuration class in your component scan? Or maybe there's an issue with your qualifier annotations?

Spring's reflection-based approach comes with significant tradeoffs:

  1. Runtime failures: Dependency issues surface only when the application runs
  2. Complex debugging: Tracking dependency flow requires navigating through annotation-based configurations
  3. Heavy startup overhead: Component scanning and proxy generation slow application startup
  4. Steep learning curve: Mastering Spring's extensive configuration options takes considerable time

Let's examine a typical Spring dependency setup:

@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;

@Autowired
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
}

// Elsewhere in your application
@RestController
public class UserController {
private final UserService userService;

@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
}

Seems straightforward—until you have multiple implementations of UserService or need to understand where each dependency comes from in a large codebase.

Enter Shank: Type-Safe Dependency Injection for Kotlin

Shank takes a fundamentally different approach to dependency injection. Instead of relying on runtime reflection, annotation processing, or "magic," Shank provides a clean, explicit, and type-safe API.

Here's the equivalent Shank implementation:

object UserModule : ShankModule {
val userRepository = single { -> PostgresUserRepository() }
val userService = single<UserService> { -> UserServiceImpl(userRepository()) }
}

class UserController {
private val userService = UserModule.userService()

fun getUser(id: String): User {
return userService.getUser(id)
}
}

The contrast is striking. With Shank:

  1. Dependencies are explicitly declared and easy to trace
  2. Compilation fails if dependencies aren't satisfied—no more runtime surprises
  3. Zero reflection means faster startup and reduced memory usage
  4. Navigation is trivial—simply Ctrl+click on userService() to see its definition

Real-World Comparison: Spring vs. Shank

Let's compare how each framework handles common dependency injection scenarios:

Scenario 1: Finding the Source of a Dependency

Spring:

  1. Locate the @Autowired field or constructor parameter
  2. Search for classes implementing the required interface
  3. Check for multiple implementations and qualifier annotations
  4. Examine component scanning configuration to ensure the implementation is detected
  5. Debug with runtime logging if the dependency still can't be located

Shank:

  1. Ctrl+click on the provider function call (e.g., userService())
  2. Immediately see the implementation in the module definition

Scenario 2: Debugging a Missing Dependency

Spring:

APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.example.UserController required a bean
of type 'com.example.UserService' that could not be found.

You must then:

  1. Check if the implementation has the correct annotation
  2. Verify component scanning is configured properly
  3. Check if there are conflicting qualifiers
  4. Add extensive debug logging

Shank:

Compilation failed:
Unresolved reference: userService

That's it. The compiler immediately tells you what's missing, and you can fix it before even running the application.

Scenario 3: Handling Circular Dependencies

Spring:

BeanCurrentlyInCreationException: Error creating bean with name 'serviceA': 
Requested bean is currently in creation: Is there an unresolvable circular reference?

Shank: The Kotlin compiler detects circular dependencies at compile time through its cycle detection, making this scenario impossible.

Measurable Advantages of Shank

Shank's advantages aren't just theoretical—they translate into real, measurable benefits:

Performance Comparison

MetricSpringShankAdvantage
Binary size8-15 MB300 KB30-50x smaller
Startup timeSecondsMilliseconds10-100x faster
Memory usageHighMinimal3-5x less memory
Compile-time safetyLimitedCompleteNo runtime DI errors

Real Application Example

We migrated a medium-sized microservice (50+ services, 200+ dependencies) from Spring to Shank. The results were remarkable:

  • 90% reduction in application startup time
  • 60% reduction in memory usage
  • Eliminated all runtime DI errors
  • Simplified codebase with explicit dependency declarations

One developer on the team remarked:

"With Spring, I spent hours debugging dependency issues. With Shank, I haven't encountered a single DI-related error in production. The code is more readable, and I can always tell where a dependency comes from."

Beyond the Basics: Advanced Shank Features

Shank isn't just simpler and faster—it's also incredibly powerful:

Type-bound Dependencies

object RepositoriesModule : ShankModule {
// Bind implementation to interface
val userRepository = single<UserRepository> { -> PostgresUserRepository() }
}

// Usage with full type safety
val repo: UserRepository = RepositoriesModule.userRepository()

Parameterized Dependencies

object CacheModule : ShankModule {
val cache = single { region: String -> Cache(region) }
}

// Different instances for different parameters
val userCache = CacheModule.cache("users")
val postCache = CacheModule.cache("posts")

Factory Overrides for Testing

@BeforeEach
fun setup() {
// Override with mock for testing
UserModule.userRepository.overrideFactory { -> mockRepository }
}

@AfterEach
fun tearDown() {
// Restore original implementation
UserModule.userRepository.restore()
}

Scoped Dependencies

object RequestModule : ShankModule {
val requestContext = scoped { requestId: String -> RequestContext(requestId) }
}

// Scoped to the provided parameter
val context1 = RequestModule.requestContext("request1")
val context2 = RequestModule.requestContext("request2")

Spring's Complexity vs. Shank's Simplicity

Let's look at how Spring and Shank handle a more complex dependency scenario:

Spring Configuration

@Configuration
@EnableCaching
@EnableScheduling
@EnableAsync
@ComponentScan("com.example")
public class AppConfig {
@Bean
@Qualifier("primary")
@Scope("singleton")
public DataSource primaryDataSource() {
return new DataSourceBuilder()
.url(env.getProperty("db.primary.url"))
.build();
}

@Bean
@Qualifier("secondary")
@Scope("singleton")
public DataSource secondaryDataSource() {
return new DataSourceBuilder()
.url(env.getProperty("db.secondary.url"))
.build();
}

@Bean
@Primary
public UserRepository userRepository(@Qualifier("primary") DataSource dataSource) {
return new JdbcUserRepository(dataSource);
}
}

// Usage
@Service
public class UserService {
@Autowired
private UserRepository userRepository;

// Or with constructor injection
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}

Shank Configuration

object DataSourceModule : ShankModule {
val primaryDataSource = single { ->
DataSourceBuilder()
.url(config().getString("db.primary.url"))
.build()
}

val secondaryDataSource = single { ->
DataSourceBuilder()
.url(config().getString("db.secondary.url"))
.build()
}
}

object RepositoryModule : ShankModule {
val userRepository = single { ->
JdbcUserRepository(DataSourceModule.primaryDataSource())
}
}

// Usage
class UserService {
private val userRepository = RepositoryModule.userRepository()
}

The Shank approach is:

  • More concise
  • More explicit about dependency relationships
  • Completely type-safe
  • Easy to navigate and understand
  • Free from runtime DI errors

Common Objections and Responses

"But Spring has a rich ecosystem!"

Spring's ecosystem is indeed extensive, but at a cost. That cost includes complexity, performance overhead, and a steep learning curve. Shank integrates seamlessly with other libraries without imposing Spring's overhead.

"Spring Boot makes configuration easier!"

Spring Boot reduces boilerplate but still relies on the same reflection-based approach with its inherent issues. Shank eliminates the need for auto-configuration by making dependencies explicit and traceable.

"Spring is widely adopted in the industry!"

While true, many teams are recognizing the advantages of more modern, Kotlin-native approaches. Shank's alignment with Kotlin's philosophy of explicitness and type safety makes it increasingly popular among Kotlin developers.

The Future of Dependency Injection

As software development evolves, the trend is clear:

  1. Moving from implicit to explicit dependency management
  2. Shifting validation from runtime to compile-time
  3. Reducing framework overhead for better performance
  4. Simplifying debugging through clear dependency tracing

Shank represents the future of dependency injection—a lightweight, performant, type-safe approach that aligns perfectly with modern Kotlin development.

Conclusion

Spring revolutionized Java development by introducing dependency injection as a first-class concept. However, in the Kotlin ecosystem, Shank represents the next evolution—offering all the benefits of dependency injection without the drawbacks of reflection, runtime errors, and performance overhead.

By adopting Shank, you're choosing:

  • Complete type safety over runtime errors
  • Explicit dependencies over "magic" autowiring
  • Lightweight performance over heavy framework overhead
  • Simple debugging over complex reflection-based issues
  • Kotlin-first design over Java legacy approaches

The question isn't whether Shank is superior to Spring for Kotlin applications—it's why you would choose to stick with Spring's complexity when a simpler, safer, and more performant alternative exists.

Ready to experience dependency injection done right? Try Shank in your next Kotlin project and discover what you've been missing.

Spring Boot vs Snitch: A Comprehensive Comparison for Modern API Development

· 16 min read
Snitch Team
Snitch Framework Maintainers

When choosing a framework for your next API project, the options can be overwhelming. Spring Boot has long been the industry standard for Java-based web services, offering a mature ecosystem and comprehensive feature set. However, Snitch has emerged as a compelling alternative for Kotlin developers seeking a more modern, lightweight approach.

In this detailed comparison, we'll explore how these two frameworks stack up across several critical dimensions, helping you make an informed decision for your next project.

Type-Safe Validation: Turning Runtime Errors into Compile-Time Safety

· 4 min read
Snitch Team
Snitch Framework Maintainers

One of the most common sources of bugs in web applications is improper handling of user input. Traditional frameworks often leave validation as an afterthought, resulting in runtime errors that could have been caught earlier. Snitch takes a different approach, making validation a first-class concern with compile-time safety.

Migration Guide: From Spring Boot to Snitch

· 7 min read
Snitch Team
Snitch Framework Maintainers

Many teams working with Kotlin find themselves using Spring Boot because it's the industry standard for Java applications. But as projects grow, they often encounter limitations with Spring's Java-centric approach when used with Kotlin. If you're considering migrating from Spring Boot to Snitch, this guide will help you navigate the transition with practical examples and a step-by-step approach.

The Inspiration Behind Snitch - Borrowing from the Best

· 8 min read
Snitch Team
Snitch Framework Maintainers

Creating a new web framework isn't a decision to take lightly. The ecosystem is already crowded with options across many languages, each with their strengths and devoted communities. So when we built Snitch, we didn't start from scratch - instead, we carefully studied what worked well in other frameworks and what didn't, then synthesized those lessons into something new that addresses the specific needs of Kotlin developers.

Unlocking Advanced Web APIs with Snitch

· 6 min read
Snitch Team
Snitch Framework Maintainers

Building production-grade HTTP APIs can be complex and time-consuming. Many frameworks offer simplicity at the cost of readability or performance when systems grow beyond simple examples. Today, I'm excited to introduce you to Snitch: a Kotlin HTTP framework that prioritizes readability and maintainability while delivering exceptional performance and a powerful feature set.

Beyond Annotations: Why Snitch's DSL Approach Improves Code Readability

· 4 min read
Snitch Team
Snitch Framework Maintainers

When examining modern web frameworks, one pattern appears consistently: the heavy use of annotations to configure behavior. From Spring's @RequestMapping to JAX-RS's @Path, annotations have become the standard way to define routes, validation, and more. But Snitch takes a different approach with its expressive DSL. Here's why that matters for your codebase.