Recent Posts
Archives

PostHeaderIcon [DevoxxPL2019] Revitalizing Behavior-Driven Development for Web Testing with Gauge and Taiko

Lecturer

Dmitry Vinnik functions as an Engineering Manager at Meta, specializing in AI/ML, where he leads teams on cutting-edge solutions. As a Lead Developer Advocate for Open Source and Business Messaging, he promotes tools enhancing productivity, with a passion for software quality and international speaking engagements.

Abstract

This review revitalizes Behavior-Driven Development for web testing, contrasting traditional frameworks with Gauge and Taiko. It explores BDD principles, Gauge’s markdown-based specifications versus Cucumber’s Gherkin, and Taiko’s scriptless automation. Through demonstrations on a todo app, it assesses methodologies for cross-team communication, test maintenance, and browser interactions, while contemplating impacts on adoption and organizational dynamics.

BDD Fundamentals: Communication Over Automation

Behavior-Driven Development bridges stakeholders by expressing behaviors in natural language, yet adoption falters from tool complexities. Dmitry contextualizes this: BDD aims for shared understanding, but frameworks like Cucumber introduce regex-heavy steps, alienating non-technical participants.

Gauge counters with markdown specs, free from rigid syntax, allowing plain English descriptions. Steps implement in languages like Java or JavaScript, fostering flexibility.

Analytically, this decouples narratives from executions, easing maintenance. Implications: enhances collaboration, though requires discipline to avoid verbosity.

Gauge Versus Cucumber: Simplifying Specifications

Cucumber’s Gherkin mandates “Given-When-Then,” parsed via regex, complicating updates. Gauge uses markdown, supporting tables and parameters natively.

For a login scenario:

“`

PostHeaderIcon [DevoxxPL2019] Mastering Kubernetes Development Within IntelliJ: Seamless Integration and Debugging

Lecturer

Ivan Portyankin works as a software engineer at Google, contributing to Google Cloud Platform and Cloud Code for IntelliJ. Based in New York City, he focuses on tools that simplify cloud-native development, with a background in enhancing developer productivity through IDE integrations.

Abstract

This discussion examines the capabilities of Google Cloud Tools for IntelliJ in streamlining Kubernetes development workflows. It covers motivations for IDE-centric approaches, conversions from plain Java apps to containerized deployments, and features like live debugging and continuous updates. Through demonstrations, it analyzes methodologies for YAML minimization, cluster interactions, and hot-swapping, while reflecting on implications for developer efficiency and Kubernetes adoption barriers.

Motivational Landscape: Bridging Code and Cluster Management

Kubernetes’ complexity often deters developers, as traditional workflows involve extensive CLI commands and YAML configurations, diverting focus from core coding. Ivan addresses this by showcasing tools that embed orchestration directly into IDEs like IntelliJ, allowing seamless transitions from local development to production deployments.

Contextually, this aligns with the rise of cloud-native paradigms, where teams seek to abstract infrastructure. Google’s Cloud Code plugin exemplifies this, supporting Java, Kotlin, Go, and other languages across JetBrains IDEs and VS Code.

Analytically, the approach reduces cognitive load: developers remain in familiar environments, avoiding context switches. Implications: accelerates iterations, lowers entry barriers for Kubernetes newcomers, fostering broader adoption in enterprises.

Application Conversion: From Monolith to Microservices

Starting with a plain Java app, Ivan demonstrates scaffolding Kubernetes manifests via Cloud Code. For a voting service, the plugin generates deployments, services, and ingresses, minimizing manual YAML edits.

Methodologically, select templates for languages like Java/Spring Boot, auto-populating fields. Deploy to clusters like GKE or Minikube directly from IDE run configurations.

For multi-language setups—Java, Kotlin/Go—the tool handles diverse runtimes, ensuring consistent deployments.

Analytically, this decouples app logic from ops, but requires accurate kubeconfig setups. Implications: enables polyglot teams, though debugging multi-pod interactions demands careful logging.

Live Debugging and Continuous Deployment: Enhancing Iteration

Cloud Code enables remote debugging on Kubernetes pods without config changes. Ivan attaches debuggers to running containers, setting breakpoints in code.

For updates, continuous mode rebuilds and redeploys on saves, hot-swapping classes where possible.

Methodologically, use Skaffold under the hood for builds; configure via skaffold.yaml for custom pipelines.

Analytically, this mirrors local debugging, bridging dev-prod gaps. Implications: shortens feedback loops, boosting productivity, though network latency can affect remote sessions.

Ecosystem Extensions and Future Directions: Beyond Basics

The plugin supports Helm for complex apps, though basic; future enhancements target better template editing.

Analytically, open-source nature invites contributions, accelerating features like multi-cluster management. Implications: democratizes Kubernetes, but skill gaps in underlying tools persist.

In essence, IDE integrations transform Kubernetes from ops burden to developer enabler.

Links:

PostHeaderIcon [KotlinConf2019] Simplifying Async APIs with Kotlin Coroutines

Tom Hanley, a senior software engineer at Toast, enthralled KotlinConf2019 with a case study on using Kotlin coroutines to tame a complex asynchronous API for an Android card reader. Drawing from his work integrating a third-party USB-connected card reader, Tom shared how coroutines transformed callback-heavy code into clean, sequential logic. His practical insights on error handling, debugging, and testing offered a roadmap for developers grappling with legacy async APIs.

Escaping Callback Hell

Asynchronous APIs often lead to callback hell, where nested callbacks make code unreadable and error-prone. Tom described the challenge of working with a third-party Android SDK for a card reader, which relied on void methods and listener interfaces for data retrieval. A naive implementation to fetch device info involved mutable variables and blocking loops, risking infinite loops and thread-safety issues. Such approaches, common with legacy APIs, complicate maintenance and scalability. Tom emphasized that coroutines offer a lifeline, allowing developers to wrap messy APIs in a clean, non-blocking interface that reads like sequential code, preserving the benefits of asynchrony.

Wrapping the Card Reader API with Coroutines

To streamline the card reader API, Tom developed a Kotlin extension that replaced callback-based interactions with suspend functions. The original API required a controller to send commands and a listener to receive asynchronous responses, such as device info or errors. By introducing a suspend getDeviceInfo function, Tom enabled callers to await results directly. This extension ensured referential transparency, where functions clearly return their results, and allowed callers to control asynchrony—waiting for completion or running tasks concurrently. The approach also enforced sequential execution for dependent operations, critical for the card reader’s connection and transaction workflows.

Communicating with Channels

Effective inter-thread communication was key to the extension’s success. Rather than relying on shared mutable variables, Tom used Kotlin channels to pass events and errors between coroutines. When the listener received device info, it sent the data to a public channel; errors were handled similarly. The controller extension used a select expression to await the first event from either the device info or error channel, throwing errors or returning results as needed. Channels, with their suspending send and receive operations, provided a thread-safe alternative to blocking queues. Despite their experimental status in Kotlin 1.3, Tom found them production-ready, supported by smooth IDE migration paths.

Mastering Exception Handling

Exception handling in coroutines requires careful design, as Tom learned through structured concurrency introduced in Kotlin 1.3. This feature enforces a parent-child relationship, where canceling a parent coroutine cancels its children. However, Tom discovered that a child’s failure propagates upward, potentially crashing the app in launch coroutines if uncaught. For async coroutines, exceptions are deferred until await is called, allowing try-catch blocks to handle them. To isolate failures, Tom used supervisorJob to prevent child cancellations from affecting siblings and coroutineScope blocks to group all-or-nothing operations, ensuring robust error recovery for the card reader’s unreliable USB connection.

Debugging and Testing Coroutines

Debugging coroutines posed initial challenges, but Tom leveraged powerful tools to simplify the process. Enabling debug mode via system properties assigns unique names to coroutines, appending them to thread names and enhancing stack traces with creation details. The debug agent, a JVM tool released post-project, tracks live coroutines and dumps their state, aiding deadlock diagnosis. For testing, Tom wrapped suspend functions in runBlocking blocks, enabling straightforward unit tests. He advised using launch and async only when concurrency is needed, marking functions as suspend to simplify testing by allowing callers to control execution context.

Moving Beyond Exceptions with Sealed Classes

Reflecting on exception handling’s complexity, Tom shifted to sealed classes for error handling. Initially, errors from the card reader were wrapped in exceptions, but frequent USB failures made catching them cumbersome. Exceptions also obscured control flow and hindered functional purity. Inspired by arguments likening exceptions to goto statements, Tom adopted domain-specific sealed classes (e.g., Success, Failure, Timeout) for each controller command’s result. This approach enforced explicit error handling via when statements, improved readability, and allowed result types to evolve independently, aligning with the card reader’s diverse error scenarios.

Links:

PostHeaderIcon [DevoxxPL2019] Micronaut Versus Spring Boot: Assessing Framework Alternatives

Lecturer

Vladimir Dejanović occupies the role of senior director for B2C technology at PVH, managing tech for fashion labels including Tommy Hilfiger and Calvin Klein. Leading the Amsterdam Java User Group as founder, he holds JavaOne Rockstar and CodeOne Star status, often presenting on Java ecosystems and patterns.

Abstract

This evaluation pits Micronaut against Spring Boot, exploring their strengths in Java app construction. It details comparison drivers, a CRUD repository task, and metrics like launch speed, resource consumption, and native compilation. Via coding sessions, it gauges philosophies, efficiency, and feature sets, while contemplating appropriateness for fresh initiatives versus legacy code.

Driving the Comparison: Libraries Versus Integrated Solutions

Deciding between modular libraries and all-inclusive frameworks shapes Java projects. Vladimir delineates: libraries afford customization but integration labor, frameworks like Spring Boot deliver ready solutions potentially at efficiency expense.

Background: Spring’s prowess incurs reflection-based costs, evident in clouds. Micronaut vows comparable might minus drawbacks, using build-time computations.

Analytically, suits service-oriented architectures needing swift boots. Ramifications: frameworks hasten prototypes, but burdens affect expansion; Micronaut’s method may streamline allocations.

Task Design and Execution: CRUD in Repositories

For contrast, Vladimir crafts a person-rating CRUD: compute from age/name, persist. Spring Boot uses annotations for models/repositories, leveraging CrudRepository’s auto-implementations.

Snippet:

@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int age;
    private int rating;
    // accessors
}

@Repository
public interface PersonRepository extends CrudRepository<Person, Long> {}

Micronaut necessitates explicit codings, annotating @Repository, implementing interfaces manually.

Analytically, Spring’s brevity accelerates, Micronaut’s clarity aids comprehension. Ramifications: Spring for quick builds; Micronaut for tuned performances.

Efficiency Metrics: Boot Times, Usage, Native Builds

Boot: Micronaut quicker from compile injections, Spring slower via runtime scans. Usage: Micronaut lighter, sans proxies.

Native: Micronaut natively compatible; Spring lacks direct backing.

Analytically, advantages Micronaut in ephemeral or constrained contexts. Ramifications: lowered cloud expenses, rapid initiations improving experiences.

Feature Landscape and Guides: Production Viability

Micronaut expands swiftly, backing Kafka, GraphQL, gRPC, discoveries. Guides/tutorials excel.

Spring Boot’s ripeness provides extensive links, but heavier.

Analytically, both facilitate rapid resolutions, Micronaut’s freshness attracts innovators. Ramifications: Micronaut for pioneers; keep Spring for established bases.

Final Appraisals: Judicious Choices

Both shine in output, Spring slightly in ease, Micronaut in efficacy. Maintain Spring legacies; ponder Micronaut for novices.

Ramifications: context-driven selections balance rapidity and extensibility.

Links:

PostHeaderIcon [DevoxxPL2019] Resilience Patterns in Microservices: Beyond Hystrix

Lecturer

Tomasz Skowroński contributes to resilience-focused libraries and speaks on fault tolerance in distributed systems.

Abstract

This overview introduces resilience patterns for microservices, transitioning from Hystrix to Resilience4j. It explains time limiters, rate limiters, bulkheads, retries, and circuit breakers, using analogies and code to demonstrate configurations and executions. It appraises integration with existing code, order of application, and higher-layer usages, while considering effects on system stability and developer explicitness.

Introducing Resilience: Patterns for Fault Tolerance

Resilience ensures responsiveness amid failures, vital in distributed setups. Tomasz analogizes to Dunkirk evacuation, where limited boats mirror API calls—use responsibly.

Hystrix, Netflix’s library, implemented circuit breakers but ceased development. Resilience4j succeeds, embracing Java 8+ functional styles sans annotations or AOP.

Analytically, this shift favors lightweight, composable resilience over monolithic commands. Implications: easier adoption in diverse stacks, reducing overhead.

Time and Rate Limiters: Controlling Execution Durations and Frequencies

Time limiters enforce timeouts on futures or suppliers, preventing indefinite waits. Configure via builders:

TimeLimiterConfig config = TimeLimiterConfig.custom()
    .timeoutDuration(Duration.ofMillis(500))
    .build();
TimeLimiter timeLimiter = TimeLimiter.of(config);
Callable<String> callable = TimeLimiter.decorateFutureSupplier(timeLimiter, () -> CompletableFuture.supplyAsync(this::slowMethod));

This decorates calls, throwing on timeouts.

Rate limiters restrict invocations per period, using permissions:

RateLimiterConfig config = RateLimiterConfig.custom()
    .limitForPeriod(50)
    .limitRefreshPeriod(Duration.ofMinutes(1))
    .timeoutDuration(Duration.ofSeconds(3))
    .build();
RateLimiter rateLimiter = RateLimiter.of("backend", config);
Runnable restrictedCall = RateLimiter.decorateRunnable(rateLimiter, this::backendMethod);

Analytically, parameters like refresh periods balance throughput and protection. Implications: prevents overloads, though misconfigurations cause premature failures.

Bulkheads and Retries: Isolating and Recovering from Failures

Bulkheads isolate via thread pools or semaphores, limiting concurrent calls:

BulkheadConfig config = BulkheadConfig.custom()
    .maxConcurrentCalls(100)
    .maxWaitDuration(Duration.ofMillis(10))
    .build();
Bulkhead bulkhead = Bulkhead.of("backend", config);
Supplier<String> decorated = Bulkhead.decorateSupplier(bulkhead, this::backendMethod);

Retries attempt failed calls, configurable for attempts and waits.

Analytically, exponential backoffs mitigate thundering herds. Implications: boosts reliability, but excessive retries amplify loads.

Circuit Breakers: Caching Failures for Protection

Circuit breakers track successes/failures in buffers, opening on thresholds to block calls, periodically probing recovery.

Configurations define states: closed (allow), open (block), half-open (test).

Analytically, sliding windows maintain recent histories. Implications: shields backends during outages, explicit via decorators.

Strategic Application: Ordering, Layers, and Myths

Order matters: circuit breakers before retries avoid futile attempts. Apply at gateways or clouds for broader protection.

Myths: not all patterns always; failure-fast over safe sans explicit fallbacks.

Implications: explicitness via decorators clarifies intent, fostering robust designs.

Links:

PostHeaderIcon [KotlinConf2019] Kotlin Coroutines: Mastering Cancellation and Exceptions with Florina Muntenescu & Manuel Vivo

Kotlin coroutines have revolutionized asynchronous programming on Android and other platforms, offering a way to write non-blocking code in a sequential style. However, as Florina Muntenescu and Manuel Vivo, both prominent Android Developer Experts then at Google, pointed out at KotlinConf 2019, the “happy path” is only part of the story. Their talk, “Coroutines! Gotta catch ’em all!” delved into the critical aspects of coroutine cancellation and exception handling, providing developers with the knowledge to build robust and resilient asynchronous applications.

Florina and Manuel highlighted a common scenario: coroutines work perfectly until an error occurs, a timeout is reached, or a coroutine needs to be cancelled. Understanding how to manage these situations—where to handle errors, how different scopes affect error propagation, and the impact of launch vs. async—is crucial for a good user experience and stable application behavior.

Structured Concurrency and Scope Management

A fundamental concept in Kotlin coroutines is structured concurrency, which ensures that coroutines operate within a defined scope, tying their lifecycle to that scope. Florina Muntenescu and Manuel Vivo emphasized the importance of choosing the right CoroutineScope for different situations. The scope dictates how coroutines are managed, particularly concerning cancellation and how exceptions are propagated.

They discussed:
* CoroutineScope: The basic building block for managing coroutines.
* Job and SupervisorJob: A Job in a coroutine’s context is responsible for its lifecycle. A key distinction is how they handle failures of child coroutines. A standard Job will cancel all its children and itself if one child fails. In contrast, a SupervisorJob allows a child coroutine to fail without cancelling its siblings or the supervisor job itself. This is critical for UI components or services where one failed task shouldn’t bring down unrelated operations. The advice often given is to use SupervisorJob when you want to isolate failures among children.
* Scope Hierarchy: How scopes can be nested and how cancellation or failure in one part of the hierarchy affects others. Understanding this is key to preventing unintended cancellations or unhandled exceptions.

Cancellation: Graceful Termination of Coroutines

Effective cancellation is vital for resource management and preventing memory leaks, especially in UI applications where operations might become irrelevant if the user navigates away. Florina and Manuel would have covered how coroutines support cooperative cancellation. This means that suspending functions in the kotlinx.coroutines library are generally cancellable; they check for cancellation requests and throw a CancellationException when one is detected.

Key points regarding cancellation included:
* Calling job.cancel() initiates the cancellation of a coroutine and its children.
* Coroutines must cooperate with cancellation by periodically checking isActive or using cancellable suspending functions. CPU-bound work in a loop that doesn’t check for cancellation might not stop as expected.
* CancellationException is considered a normal way for a coroutine to complete due to cancellation and is typically not logged as an unhandled error by default exception handlers.

Exception Handling: Catching Them All

Handling exceptions correctly in asynchronous code can be tricky. Florina and Manuel’s talk aimed to clarify how exceptions propagate in coroutines and how they can be caught.
They covered:
* launch vs. async:
* With launch, exceptions are treated like uncaught exceptions in a thread—they propagate up the job hierarchy. If not handled, they can crash the application (depending on the root scope’s context and CoroutineExceptionHandler).
* With async, exceptions are deferred. They are stored within the Deferred result and are only thrown when await() is called on that Deferred. This means if await() is never called, the exception might go unnoticed unless explicitly handled.
* CoroutineExceptionHandler: This context element can be installed in a CoroutineScope to act as a global handler for uncaught exceptions within coroutines started by launch in that scope. It allows for centralized error logging or recovery logic. They showed examples of how and where to install this handler effectively, for example, in the root coroutine or as a direct child of a SupervisorJob to catch exceptions from its children.
* try-catch blocks: Standard try-catch blocks can be used within a coroutine to handle exceptions locally, just like in synchronous code. This is often the preferred way to handle expected exceptions related to specific operations.

The speakers stressed that uncaught exceptions will always propagate, so it’s crucial to “catch ’em all” to avoid unexpected behavior or crashes. Their presentation aimed to provide clear patterns and best practices to ensure that developers could confidently manage both cancellation and exceptions, leading to more robust and user-friendly Kotlin applications.

Links:

PostHeaderIcon [DevoxxPL2019] Evaluating Micronaut Versus Spring Boot: A Framework Comparison

Lecturer

Vladimir Dejanović holds the position of senior director of B2C technology at PVH, overseeing fashion tech initiatives for brands like Tommy Hilfiger and Calvin Klein. As founder and leader of the Amsterdam Java User Group, he is a JavaOne Rockstar and CodeOne Star, frequently speaking on Java frameworks and architectures.

Abstract

This assessment contrasts Micronaut and Spring Boot, scrutinizing their capabilities in building Java applications. It outlines motivations for comparison, details a challenge involving repository implementations, and evaluates aspects like startup time, memory usage, and GraalVM compatibility. Through live demonstrations, it appraises design philosophies, performance metrics, and ecosystem maturity, while deliberating suitability for new versus existing projects.

Motivational Framework: Choosing Between Toolkits and Ecosystems

Selecting between library assemblages and comprehensive frameworks defines modern Java development. Vladimir articulates this dichotomy: libraries offer flexibility but demand integration, while frameworks like Spring Boot provide batteries-included convenience at potential runtime costs.

Context: Spring’s dominance stems from its power, yet expenses in reflection and startup manifest in cloud environments. Micronaut promises equivalent functionality sans drawbacks, leveraging compile-time processing.

Analytically, this addresses microservices’ needs for lightweight, fast-starting apps. Implications: frameworks accelerate prototyping, but overheads impact scaling; Micronaut’s approach could optimize resource utilization.

Challenge Setup and Implementation: Repository Patterns Examined

To compare, Vladimir devises a repository challenge: implement CRUD for persons with ratings from age and name. Spring Boot employs annotations for entities and repositories, extending CrudRepository for magic implementations.

Code:

@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int age;
    private int rating;
    // getters/setters
}

@Repository
public interface PersonRepository extends CrudRepository<Person, Long> {}

Micronaut requires manual implementations, using @Repository and extending interfaces, coding CRUD in classes.

Analytically, Spring’s conciseness boosts productivity, while Micronaut’s explicitness aids understanding. Implications: Spring suits rapid development; Micronaut favors control in performance-critical scenarios.

Performance Benchmarks: Startup, Memory, and Native Compilation

Startup: Micronaut launches faster due to compile-time dependency injection, versus Spring’s runtime reflection. Memory: Micronaut consumes less, avoiding proxies.

GraalVM: Micronaut compiles natively out-of-box; Spring lacks seamless support.

Analytically, these metrics favor Micronaut in serverless or resource-constrained setups. Implications: reduced costs in cloud billing, faster cold starts enhancing user experience.

Ecosystem and Documentation: Readiness for Production

Micronaut’s ecosystem grows rapidly, supporting Kafka, GraphQL, gRPC, and service discovery. Documentation excels with guides and tutorials.

Spring Boot’s maturity offers vast integrations, but at higher overheads.

Analytically, both enable quick solutions, but Micronaut’s modernity appeals for greenfield projects. Implications: Micronaut suits innovation; retain Spring for legacy stability.

Concluding Evaluations: Strategic Framework Selection

Both excel in productivity, with Spring edging in simplicity, Micronaut in efficiency. Retain existing Spring; consider Micronaut for new endeavors.

Implications: informed choices optimize for context, balancing speed and scalability.

Links:

PostHeaderIcon [KotlinConf2019] Kotless: A Kotlin-Native Approach to Serverless with Vladislav Tankov

Serverless computing has revolutionized how applications are deployed and scaled, but it often comes with its own set of complexities, including managing deployment DSLs like Terraform or CloudFormation. Vladislav Tankov, then a Software Developer at JetBrains, introduced Kotless at KotlinConf 2019 as a Kotlin Serverless Framework designed to simplify this landscape. Kotless aims to eliminate the need for external deployment DSLs by allowing developers to define serverless applications—including REST APIs and event handling—directly within their Kotlin code using familiar annotations. The project can be found on GitHub at github.com/JetBrains/kotless.

Vladislav’s presentation provided an overview of the Kotless Client API, demonstrated its use with a simple example, and delved into the architecture and design concepts behind its code-to-deployment pipeline. The core promise of Kotless is to make serverless computations easily understandable for anyone familiar with event-based architectures, particularly those comfortable with JAX-RS-like annotations.

Simplifying Serverless Deployment with Kotlin Annotations

The primary innovation of Kotless, as highlighted by Vladislav Tankov, is its ability to interpret Kotlin code and annotations to automatically generate the necessary deployment configurations for cloud providers like AWS (initially). Instead of writing separate configuration files in YAML or other DSLs, developers can define their serverless functions, API gateways, permissions, and scheduled events using Kotlin annotations directly on their functions and classes.

For example, creating a REST API endpoint could be as simple as annotating a Kotlin function with @Get("/mypath"). Kotless then parses these annotations during the build process and generates the required infrastructure definitions, deploys the lambdas, and configures the API Gateway. This approach significantly reduces boilerplate and the cognitive load associated with learning and maintaining separate infrastructure-as-code tools. Vladislav emphasized that a developer only needs familiarity with these annotations to create and deploy a serverless REST API application.

Architecture and Code-to-Deployment Pipeline

Vladislav Tankov provided insights into the inner workings of Kotless, explaining its architecture and the pipeline that transforms Kotlin code into a deployed serverless application. This process generally involves:
1. Annotation Processing: During compilation, Kotless processes the special annotations in the Kotlin code to understand the desired serverless architecture (e.g., API routes, event triggers, scheduled tasks).
2. Terraform Generation (Initially): Kotless then generates the necessary infrastructure-as-code configurations (initially using Terraform as a backend for AWS) based on these processed annotations. This includes defining Lambda functions, API Gateway resources, IAM roles, and event source mappings.
3. Deployment: Kotless handles the deployment of these generated configurations and the application code to the target cloud provider.

He also touched upon optimizations built into Kotless, such as “outer warming” of lambdas to reduce cold starts and optimizing lambdas by size. This focus on performance and ease of use is central to Kotless’s philosophy. The framework aims to abstract away the underlying complexities of serverless platforms, allowing developers to concentrate on their application logic.

Future Directions and Multiplatform Aspirations

Looking ahead, Vladislav Tankov discussed the future roadmap for Kotless, including ambitious plans for supporting Kotlin Multiplatform Projects (MPP). This would allow developers to choose different runtimes for their lambdas—JVM, JavaScript, or even Kotlin/Native—depending on the task and performance requirements. Supporting JavaScript lambdas, for example, could open up compatibility with platforms like Google Cloud Platform more broadly, which at the time had better support for JavaScript runtimes than JVM for serverless functions.

Other planned enhancements included extended event handling for custom events on AWS and other platforms, and continued work on performance optimizations. The vision for Kotless was to provide a comprehensive and flexible serverless solution for Kotlin developers, empowering them to build efficient and scalable cloud-native applications with minimal friction. Vladislav encouraged attendees to try Kotless and contribute to its development, positioning it as a community-driven effort to improve the Kotlin serverless experience.

Links:

PostHeaderIcon [DevoxxPL2019] Pursuing Software Excellence: Satirical Reflections on Quality Assurance

Lecturer

Chet Haase, a comedy writer and former software engineer at Google specializing in Android, has authored several books on the platform’s history and development. With decades in Silicon Valley, he now focuses on humorous presentations and writings, drawing from his engineering background.

Abstract

This satirical examination critiques conventional quality pursuits in software, proposing absurd strategies to eliminate defects entirely. It defines quality through bug absence, introduces mock equations and metrics, and debunks testing myths via humor. Through exaggerated organizational restructurings and release tactics, it highlights methodologies’ absurdities, contextualizing within engineering cultures, and pondering satirical implications for productivity and perceptions.

Redefining Quality: Beyond Bugs and Toward Perfection

Quality in software often evokes rigorous processes, yet Chet humorously redefines it as absolute bug elimination, achievable only by ceasing code production. He parodies presentations with Greek-lettered equations, positing quality inversely proportional to defects—zero bugs yielding infinite quality.

Contextually, this lampoons metric obsessions, where charts depict declining bugs equating rising excellence. Analytically, it underscores defect inevitability in active development, as coding introduces errors.

Implications: satirizes pursuits of unattainable perfection, urging balanced approaches where quality stems from iterative improvements, not cessation.

Organizational Overhauls: Eliminating Defect Sources

Chet proposes radical restructurings: fire engineers, as they author bugs. Managers, attending meetings sans coding, produce none—hire more. Sales rebrands existing releases (e.g., 2.3 as 3.0), avoiding new defects.

Methodologically, this “bug equilibrium” maintains status quo, parodying stagnation fears. Analytically, highlights engineers’ value despite imperfections, as innovation requires risk.

Consequences: mocks efficiency quests ignoring human elements, implying true quality demands collaboration, not elimination.

Misconceptions and Practical Jests: Testing Fallacies Exposed

Common myths: testing ensures quality—Chet counters, tests merely verify existing quality, ineffective sans code. Quality as quantifiable—satirizes certificates as superficial validations.

Analytically, underscores qualitative aspects like usability over metrics. Implications: encourages holistic views, where quality encompasses user satisfaction beyond defect counts.

Satirical Ramifications: Lessons in Absurdity

Chet’s humor reveals engineering absurdities: over-reliance on processes stifles creativity. Implications: fosters self-reflection, promoting balanced methodologies valuing people over perfection.

In essence, quality eludes simplistic fixes, demanding nuanced, human-centered strategies.

Links:

PostHeaderIcon [DevoxxPL2019] Crafting Effective Automated Tests: Insights Beyond Conventional Wisdom

Lecturer

Jacek Milewski serves as a senior software developer at Circle K, where he focuses on backend Java development in domains like fuel retail and electric vehicles. As a trainer at Bottega IT Minds, he conducts sessions on domain-driven design and software architecture, drawing from his experience as a consultant, speaker, and mentor in the IT community.

Abstract

This analysis investigates approaches to automated unit and integration testing in modular applications, emphasizing practical techniques for ensuring business logic integrity. It explores test builders for entity construction, in-memory versus real repositories, and the role of test-driven development in maintaining quality. Through a live-coded example of rating calculations based on age and name length, it evaluates methodologies for edge case coverage, assertion strategies, and the balance between speed and thoroughness, while considering implications for development velocity and software reliability.

Establishing Test Foundations: From Basic Assertions to Modular Design

Automated testing forms the bedrock of reliable software, yet many practitioners grapple with adapting strategies to evolving ecosystems. Jacek commences by underscoring the perpetual relevance of testing, as technological advancements continually introduce new challenges. His methodology revolves around a simple yet comprehensive example: computing a person’s rating from age and name length, where ratings range from 0 to 100, with penalties for ages under 18 or over 65, and bonuses for longer names.

Initial tests focus on isolated units, such as a rating calculator class. Here, inputs are mocked or directly provided, verifying outputs against expectations. For instance, a test might instantiate a person with age 20 and name “John Doe,” asserting the rating equals age plus name length, capped at 100. This isolates logic, ensuring purity without external dependencies.

As complexity grows, modularization becomes key. Jacek advocates separating concerns: entities hold data, services compute logic, repositories persist state. Tests then target these layers individually, using builders to construct test data fluently. A PersonBuilder might chain methods like withAge(25).withName(“Alice”).build(), promoting readability and reuse.

Contextually, this stems from real-world projects at Circle K, where business rules like vehicle charging require verifiable implementations. Analytically, such isolation accelerates feedback loops, catching defects early. However, over-isolation risks missing integration issues, necessitating complementary tests.

Implications extend to team dynamics: standardized builders reduce onboarding time, fostering consistency. Yet, excessive abstraction can obscure intent, demanding balance.

Integrating Dependencies: Balancing Unit and Integration Testing

Transitioning to dependencies, Jacek differentiates unit tests—focusing on isolated behavior—from integration tests, verifying interactions. For persistence, in-memory repositories simulate databases, allowing rapid execution without external setups.

In the rating scenario, a service saves rated persons to a repository. Unit tests inject mock repositories, asserting save invocations and contents. Code might resemble:

PersonBuilder builder = new PersonBuilder();
Person person = builder.withAge(30).withName("Bob").build();
RatingService service = new RatingService(new InMemoryRepository());
service.calculateAndSave(person);
assertEquals(1, repository.size());
assertEquals(34, repository.get(0).getRating());

This confirms logic without I/O overhead.

For integration, swap to real repositories (e.g., JPA with H2), reusing test structures. Jacek copies unit tests, altering only the injected repository, ensuring end-to-end validation with minimal duplication.

Methodologically, this dual approach leverages TDD: write failing tests, implement minimally to pass, refactor safely. Failing tests validate coverage—green from inception might overlook assertions.

Analytically, in-memory speeds iterations, while real databases catch schema mismatches. Implications: enhanced confidence in deployments, though integration suites slow CI pipelines, suggesting selective execution.

Optimizing for Development Speed: Dispelling Myths on Testing Overhead

A prevalent myth posits testing impedes velocity, yet Jacek counters with empirical observations: initial setups invest time, but yield dividends in maintainability. Without tests, early features deploy swiftly, but regressions mount, stalling progress.

Contrastingly, test-first approaches start slower—configuring builders, mocks—but sustain pace, as refactors preserve functionality. In his experience, untested codebases accrue debt, while tested ones enable fearless enhancements.

Methodologically, focus on meaningful assertions: verify behaviors, not implementations. For empty repositories, assert isEmpty() post-setup, confirming state.

Analytically, coverage metrics mislead if superficial; aim for edge cases like invalid ages or names. Implications: teams adopting this outpace untested counterparts long-term, delivering quality sustainably.

Broader Ramifications: Testing as a Catalyst for Quality Delivery

Testing transcends verification, shaping designs toward modularity. Jacek’s Circle K tenure illustrates: robust tests facilitate microservices evolution, aligning with business agility in retail.

Yet, no universal formula exists; adapt to domains—unit for logic, integration for persistence. Implications: cultivates culture valuing prevention over remediation, elevating software craftsmanship.

In summation, these practices, honed through experience, empower developers to deliver verifiable value efficiently.

Links: