<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dominik Kovács</title>
    <description>The latest articles on DEV Community by Dominik Kovács (@solodev-sk).</description>
    <link>https://dev.to/solodev-sk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4089322%2F9e54b26d-f41d-41dd-9645-0b6128cb56e2.png</url>
      <title>DEV Community: Dominik Kovács</title>
      <link>https://dev.to/solodev-sk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/solodev-sk"/>
    <language>en</language>
    <item>
      <title>Keeping dev-only beans out of the jar</title>
      <dc:creator>Dominik Kovács</dc:creator>
      <pubDate>Fri, 21 Aug 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/solodev-sk/keeping-dev-only-beans-out-of-the-jar-1a4e</link>
      <guid>https://dev.to/solodev-sk/keeping-dev-only-beans-out-of-the-jar-1a4e</guid>
      <description>&lt;h2&gt;
  
  
  The setup code nobody wants in production
&lt;/h2&gt;

&lt;p&gt;Some infrastructure has to be created before the application can use it, and who creates it depends&lt;br&gt;
on where the application runs. DynamoDB is a clear case. In production the tables are provisioned by&lt;br&gt;
Terraform or CDK before anything deploys. Locally and in tests, nothing has provisioned them, so the&lt;br&gt;
application has to do it itself or immediately fail on a missing table.&lt;/p&gt;

&lt;p&gt;There is nothing built in to do it. Relational databases get &lt;code&gt;schema.sql&lt;/code&gt; and Flyway, but the AWS&lt;br&gt;
SDK has no equivalent, so the table creation ends up as an &lt;code&gt;ApplicationRunner&lt;/code&gt; with a profile&lt;br&gt;
limiting where it runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CatalogConfiguration.java&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="nd"&gt;@Profile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dev | test"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;ApplicationRunner&lt;/span&gt; &lt;span class="nf"&gt;createProductTable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DynamoDbEnhancedClient&lt;/span&gt; &lt;span class="n"&gt;enhancedClient&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;enhancedClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"product"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TableSchema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromBean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createTable&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TableSchema.fromBean&lt;/code&gt; reads the key and attributes off the annotated &lt;code&gt;Product&lt;/code&gt; class, so the table&lt;br&gt;
is created from the same mapping the application already uses to read and write it.&lt;/p&gt;

&lt;p&gt;This works, and it ships. The runner is in the jar and &lt;code&gt;createTable&lt;/code&gt; is in the jar, so the guarantee&lt;br&gt;
is only as good as the active profiles staying right on every environment forever. A shared base&lt;br&gt;
configuration that carries &lt;code&gt;dev&lt;/code&gt;, a copied Helm chart, a machine where someone exported&lt;br&gt;
&lt;code&gt;SPRING_PROFILES_ACTIVE&lt;/code&gt; and forgot, and the code is live against a real table. Nothing warns you,&lt;br&gt;
because as far as Spring is concerned the bean did what it was told.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the code lives, not what the profile says
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;src/test/java&lt;/code&gt; compiles to &lt;code&gt;target/test-classes&lt;/code&gt;, and the Spring Boot plugin does not package that&lt;br&gt;
directory. Move the table creation there and it is not in the artifact at all.&lt;/p&gt;

&lt;p&gt;Building the same project with that bean in &lt;code&gt;src/test/java&lt;/code&gt; gives a jar containing only the&lt;br&gt;
application code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BOOT-INF/classes/com/example/catalog/CatalogApplication.class
BOOT-INF/classes/com/example/catalog/Product.class
BOOT-INF/classes/com/example/catalog/ProductController.class
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No table creation anywhere in it. Test-scoped dependencies are absent from the runtime classpath&lt;br&gt;
too, so a fake implementation that needs a container library does not drag it into production&lt;br&gt;
either.&lt;/p&gt;

&lt;p&gt;That is a different kind of guarantee from the profile version. A profile is evaluated at runtime,&lt;br&gt;
which means the code is present and something decided not to call it. A classpath is decided when&lt;br&gt;
the artifact is built. Production cannot create the table because the bytecode is not there, and&lt;br&gt;
no configuration mistake can put it back.&lt;/p&gt;

&lt;p&gt;The shape that falls out of this puts everything the application ships on one side and everything&lt;br&gt;
that only runs on a developer machine on the other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/main/java/com/example/catalog/
    CatalogApplication.java
    Product.java
    ProductController.java
src/main/resources/
    application.yaml
src/test/java/com/example/catalog/
    TestCatalogApplication.java
    LocalDynamoDbConfiguration.java
    ProductRepositoryTest.java
src/test/resources/
    application.yaml
    application-dev.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; &lt;code&gt;TestCatalogApplication&lt;/code&gt; is the local entry point, and the only &lt;code&gt;main&lt;/code&gt; method that knows&lt;br&gt;
about any of this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; &lt;code&gt;LocalDynamoDbConfiguration&lt;/code&gt; holds the container and the table setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.&lt;/strong&gt; &lt;code&gt;ProductRepositoryTest&lt;/code&gt; is an ordinary test that imports that same configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.&lt;/strong&gt; This &lt;code&gt;application.yaml&lt;/code&gt; needs care. Both files answer to &lt;code&gt;classpath:/application.yaml&lt;/code&gt; and&lt;br&gt;
test classes come first, so it does not merge with the shipped file, it hides it. Anything the&lt;br&gt;
application needs in every environment has to be repeated here or moved into a profile file&lt;br&gt;
alongside it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5.&lt;/strong&gt; &lt;code&gt;application-dev.yaml&lt;/code&gt; carries the ports and URLs that only make sense on a laptop.&lt;/p&gt;

&lt;p&gt;Nothing under &lt;code&gt;src/test&lt;/code&gt; is in the jar, which means the division is not a naming convention that has&lt;br&gt;
to be policed. The build enforces it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Starting the application from your tests
&lt;/h2&gt;

&lt;p&gt;Moving that bean raises the obvious problem. &lt;code&gt;spring-boot:run&lt;/code&gt; uses the main classpath, so the bean&lt;br&gt;
you just moved is invisible when you start the application locally, which is exactly when you wanted&lt;br&gt;
it.&lt;/p&gt;

&lt;p&gt;Boot has a second goal for this. &lt;code&gt;spring-boot:test-run&lt;/code&gt; starts the application with test classes and&lt;br&gt;
test dependencies included, and it prefers a main class found in the test classes directory. So the&lt;br&gt;
launcher lives in &lt;code&gt;src/test/java&lt;/code&gt; next to everything else that only dev needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TestCatalogApplication.java&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestCatalogApplication&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SpringApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;CatalogApplication:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LocalDynamoDbConfiguration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SpringApplication.from&lt;/code&gt; runs the real application and adds beans to it. The production entry point&lt;br&gt;
is untouched, and there is no second &lt;code&gt;@SpringBootApplication&lt;/code&gt; to keep in sync.&lt;/p&gt;

&lt;p&gt;What it adds is the container, and the table creation that depends on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LocalDynamoDbConfiguration.java&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@TestConfiguration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxyBeanMethods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LocalDynamoDbConfiguration&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nd"&gt;@ServiceConnection&lt;/span&gt;
    &lt;span class="nc"&gt;LocalStackContainer&lt;/span&gt; &lt;span class="nf"&gt;localStack&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LocalStackContainer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DockerImageName&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"localstack/localstack:4"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nc"&gt;ApplicationRunner&lt;/span&gt; &lt;span class="nf"&gt;createProductTable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DynamoDbEnhancedClient&lt;/span&gt; &lt;span class="n"&gt;enhancedClient&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;enhancedClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"product"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TableSchema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromBean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createTable&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; &lt;code&gt;@TestConfiguration&lt;/code&gt; applies only where something imports it, which is the launcher above.&lt;br&gt;
Nothing here is picked up by starting the application normally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; &lt;code&gt;@ServiceConnection&lt;/code&gt; is what removes the configuration. Spring Cloud AWS contributes a&lt;br&gt;
factory for LocalStack, so the endpoint and credentials of the started container become the AWS&lt;br&gt;
client configuration with nothing written in &lt;code&gt;application.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Importing by name is also what lets a test reuse the whole setup. An integration test that wants the&lt;br&gt;
same container and the same table asks for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ProductRepositoryTest.java&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@SpringBootTest&lt;/span&gt;
&lt;span class="nd"&gt;@Import&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LocalDynamoDbConfiguration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductRepositoryTest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local run and the tests that opt in now share one definition of what the environment looks like,&lt;br&gt;
and tests that do not import it are unaffected. That reuse is the practical argument for putting the&lt;br&gt;
configuration in &lt;code&gt;src/test/java&lt;/code&gt; rather than somewhere only the application can reach.&lt;/p&gt;

&lt;p&gt;Now one command boots the whole thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn spring-boot:test-run &lt;span class="nt"&gt;-Dspring-boot&lt;/span&gt;.run.profiles&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LocalStack starts, &lt;code&gt;@ServiceConnection&lt;/code&gt; points the DynamoDB client at it, the runner creates the&lt;br&gt;
table, and the application comes up talking to a real DynamoDB implementation. Gradle has the same&lt;br&gt;
thing as &lt;code&gt;bootTestRun&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spring-boot:test-run&lt;/code&gt; activates no profile on its own, so &lt;code&gt;dev&lt;/code&gt; has to be named. On the command&lt;br&gt;
line it applies to local runs only, and the test suite keeps whatever profiles its tests ask for.&lt;/p&gt;
&lt;h2&gt;
  
  
  The dev profile file belongs there too
&lt;/h2&gt;

&lt;p&gt;Beans are not the only thing that leaks. Think about what a dev profile file actually holds. A port&lt;br&gt;
chosen so two services can run side by side, third-party URLs pointing at a sandbox or a stub on&lt;br&gt;
localhost, a log level nobody wants in production, credentials that are fake precisely because the&lt;br&gt;
services they unlock are fake. None of it is meaningful outside the machine it was written for, and&lt;br&gt;
&lt;code&gt;src/main/resources/application-dev.yaml&lt;/code&gt; ships every line of it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/test/resources&lt;/code&gt; is left out of the jar exactly like &lt;code&gt;src/test/java&lt;/code&gt;, so that is where the file&lt;br&gt;
belongs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;src/test/resources/application-dev.yaml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9090&lt;/span&gt;
&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;serviceclient&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;pricing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;base-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:9091&lt;/span&gt;
&lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;com.example.catalog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DEBUG&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outbound HTTP is the part worth dwelling on, because tests and local development want different&lt;br&gt;
things from the same client. A test stubs the response, since it is asserting on the caller rather&lt;br&gt;
than the callee. Running locally you usually want the request to actually go somewhere, either a&lt;br&gt;
service started on another port or a stub server standing in for it.&lt;/p&gt;

&lt;p&gt;The key under &lt;code&gt;serviceclient&lt;/code&gt; is the group name the client was declared with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ImportHttpServices&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"pricing"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PricingClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starting with &lt;code&gt;dev&lt;/code&gt; active reads the file, so the client issues real requests to port 9091. The test&lt;br&gt;
suite does not activate it, so the same client stays stubbed there. Two behaviours from one&lt;br&gt;
declaration, and none of those local values are in the artifact.&lt;/p&gt;

&lt;p&gt;Production configuration stays where it always was, in &lt;code&gt;src/main/resources/application.yaml&lt;/code&gt;,&lt;br&gt;
pointing at the real service. There is no &lt;code&gt;dev&lt;/code&gt; block sitting next to it for somebody to activate by&lt;br&gt;
accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  What profiles are still good for
&lt;/h2&gt;

&lt;p&gt;None of this makes profiles the wrong tool. They are the wrong tool for one specific job, which&lt;br&gt;
is keeping code out of production, because they answer at runtime a question the build already&lt;br&gt;
knows the answer to. Choosing between local and test behaviour, as &lt;code&gt;dev&lt;/code&gt; does above, is exactly&lt;br&gt;
what they are for, and it works the same inside &lt;code&gt;src/test&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The distinction worth keeping is between choosing behaviour and excluding code. Profiles choose.&lt;br&gt;
Only the classpath excludes.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is not enough
&lt;/h2&gt;

&lt;p&gt;A deployed environment runs the real jar. Staging, a shared dev cluster, a preview deployment, all&lt;br&gt;
of them get the artifact with &lt;code&gt;target/test-classes&lt;/code&gt; left out, so a bean in &lt;code&gt;src/test/java&lt;/code&gt; cannot&lt;br&gt;
help them. If a deployed environment genuinely needs to create its own tables, this technique does&lt;br&gt;
not reach it, and the honest options are a separate module that only that environment depends on,&lt;br&gt;
or a profile with the leak risk accepted and guarded.&lt;/p&gt;

&lt;p&gt;The technique fits the case where the code is for the machine doing the building. Local runs and the&lt;br&gt;
test suite both use the test classpath, which is why one bean can serve both, and why production&lt;br&gt;
never has to be trusted to skip it.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>testing</category>
    </item>
    <item>
      <title>Vertical slices in Spring Boot, and how to test them</title>
      <dc:creator>Dominik Kovács</dc:creator>
      <pubDate>Fri, 21 Aug 2026 09:07:00 +0000</pubDate>
      <link>https://dev.to/solodev-sk/vertical-slices-in-spring-boot-and-how-to-test-them-f57</link>
      <guid>https://dev.to/solodev-sk/vertical-slices-in-spring-boot-and-how-to-test-them-f57</guid>
      <description>&lt;h2&gt;
  
  
  Where did the feature go
&lt;/h2&gt;

&lt;p&gt;Most Spring Boot projects start with the structure everyone recognises.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;controller/
service/
repository/
dto/
entity/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads well when there are four classes in it. The problem arrives later, when adding one feature&lt;br&gt;
means editing a controller here, a method on a service there, two DTOs in a third package, and a&lt;br&gt;
repository query in a fourth. Nothing about that change is complicated. It is just spread out.&lt;/p&gt;

&lt;p&gt;The alternative is to make the feature the unit of organisation rather than the technical role.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;order/
    OrderController.java
    OrderService.java
    OrderRepository.java
    OrderRequest.java
    OrderResponse.java
customer/
payment/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything an order needs lives in &lt;code&gt;order&lt;/code&gt;. That is a vertical slice, one business capability cut&lt;br&gt;
through every technical layer it happens to use.&lt;/p&gt;
&lt;h2&gt;
  
  
  The feature owns its whole stack
&lt;/h2&gt;

&lt;p&gt;The interesting consequence is not the folder. It is that the classes inside stop being &lt;code&gt;public&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In a layered layout a service in &lt;code&gt;service&lt;/code&gt; has to be visible to a controller in &lt;code&gt;controller&lt;/code&gt;, so&lt;br&gt;
every one of those types is public, which in Java means available to the entire codebase. You wanted&lt;br&gt;
"the controller may call the service" and the language made you write "anyone may call the service".&lt;/p&gt;

&lt;p&gt;Put them in the same package and the calls stop crossing a boundary at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OrderService.java&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;payments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;
    &lt;span class="nc"&gt;OrderResponse&lt;/span&gt; &lt;span class="nf"&gt;place&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorise&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OrderResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;OrderController.java&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nc"&gt;OrderController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@PostMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;OrderResponse&lt;/span&gt; &lt;span class="nf"&gt;place&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestBody&lt;/span&gt; &lt;span class="nc"&gt;OrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;place&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;public&lt;/code&gt; on either. &lt;code&gt;customer&lt;/code&gt; cannot import &lt;code&gt;OrderService&lt;/code&gt;, and not as a matter of policy. The&lt;br&gt;
compiler rejects it, on every machine, including the one belonging to whoever joins next year and&lt;br&gt;
never read the team conventions.&lt;/p&gt;

&lt;p&gt;That is the part a layered structure cannot give you. It has to expose every layer to the next, so&lt;br&gt;
"no feature depends on another feature" stays a convention you check in review rather than something&lt;br&gt;
the build enforces.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PaymentGateway&lt;/code&gt; is the one type in there that comes from outside the feature, which is a problem of&lt;br&gt;
its own and gets a section further down.&lt;/p&gt;
&lt;h2&gt;
  
  
  What still has to be shared
&lt;/h2&gt;

&lt;p&gt;Two things usually escape the feature package, and it is worth being deliberate about both.&lt;/p&gt;

&lt;p&gt;The first is the domain model, when several features genuinely write to the same rows. Those&lt;br&gt;
invariants belong to the domain, not to whichever feature touched them last, so they get a home of&lt;br&gt;
their own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;order/
    domain/
        Order.java
        OrderItem.java
        OrderRepository.java
    OrderController.java
    OrderService.java
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Java package visibility does not nest, which surprises people. As far as the compiler is concerned&lt;br&gt;
&lt;code&gt;order.domain&lt;/code&gt; is a different package from &lt;code&gt;order&lt;/code&gt;, with no privileged access to it, and there is no&lt;br&gt;
modifier meaning "visible to my subpackages". Move &lt;code&gt;Order&lt;/code&gt; down a level and it has to become&lt;br&gt;
public. That is a real cost, and a reason not to introduce subpackages until something forces you.&lt;/p&gt;

&lt;p&gt;The second is genuine infrastructure, meaning security configuration, error handling, or an&lt;br&gt;
interceptor that adds a request identifier to the logging context. None of that is a feature, and&lt;br&gt;
pretending otherwise helps nobody.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;web/
    SecurityConfiguration.java
    ApiExceptionHandler.java
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What should not escape is a helper extracted because two features looked similar once. A shared&lt;br&gt;
&lt;code&gt;OrderUtils&lt;/code&gt; that both &lt;code&gt;order&lt;/code&gt; and &lt;code&gt;payment&lt;/code&gt; depend on has coupled them together in a way the&lt;br&gt;
package structure now hides. Duplication between features is usually cheaper than a shared&lt;br&gt;
abstraction serving two masters, and the third occurrence tells you far more about the right shape&lt;br&gt;
than the second one does.&lt;/p&gt;
&lt;h2&gt;
  
  
  When one feature needs another
&lt;/h2&gt;

&lt;p&gt;Placing an order has to authorise a payment. Payments are their own feature, so what does &lt;code&gt;order&lt;/code&gt;&lt;br&gt;
call?&lt;/p&gt;

&lt;p&gt;The tempting answer is to inject &lt;code&gt;payment&lt;/code&gt;'s service directly. That works, and it quietly undoes the&lt;br&gt;
thing you just built. &lt;code&gt;payment&lt;/code&gt; now has to make that class public, &lt;code&gt;order&lt;/code&gt; compiles against&lt;br&gt;
&lt;code&gt;payment&lt;/code&gt;'s internals, and you have a dependency the package structure claims does not exist.&lt;/p&gt;

&lt;p&gt;Two options keep the boundary intact.&lt;/p&gt;

&lt;p&gt;The first is to publish a deliberately small interface from the feature being called, and treat that&lt;br&gt;
as its contract rather than an accident of its implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment/
    PaymentGateway.java        &amp;lt;- public, the contract
    PaymentService.java        &amp;lt;- package-private, the implementation
    StripeClient.java          &amp;lt;- package-private
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;order&lt;/code&gt; depends on &lt;code&gt;PaymentGateway&lt;/code&gt; and nothing else. The one public type is the price of the&lt;br&gt;
dependency, and because it is the only one, it is a decision somebody had to make on purpose rather&lt;br&gt;
than a side effect of layering.&lt;/p&gt;

&lt;p&gt;The second is to invert it. Publish an event from &lt;code&gt;order&lt;/code&gt; and let &lt;code&gt;payment&lt;/code&gt; subscribe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OrderService.java&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="nc"&gt;OrderResponse&lt;/span&gt; &lt;span class="nf"&gt;place&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publishEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderPlaced&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OrderResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;order&lt;/code&gt; knows nothing about payments at all, and &lt;code&gt;payment&lt;/code&gt; decides for itself what to do when an&lt;br&gt;
order appears. That is a stronger boundary, and it costs you the ability to reason about the whole&lt;br&gt;
operation in one place. Use it when the reaction is genuinely optional or asynchronous, and the&lt;br&gt;
interface when the caller needs the result before it can continue.&lt;/p&gt;

&lt;p&gt;Either way, a feature calling another feature should be visible, deliberate and narrow. What you are&lt;br&gt;
avoiding is not the dependency itself, it is the dependency nobody chose.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tests that mirror the structure
&lt;/h2&gt;

&lt;p&gt;Here is the part that changes day to day. Because the feature is a package, its tests are the same&lt;br&gt;
package, and a failing test names the broken capability rather than a layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/test/java/com/example/shop/
    order/
        OrderSliceTest.java
    customer/
        CustomerSliceTest.java
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests living in the same package is also what makes package-private classes testable without&lt;br&gt;
loosening anything. Maven and Gradle both put &lt;code&gt;src/test/java&lt;/code&gt; on the same package namespace, so the&lt;br&gt;
test sees the class while the rest of the application still cannot.&lt;/p&gt;

&lt;p&gt;The test that follows from this packaging is one per feature. Name the classes of the feature and&lt;br&gt;
run the whole thing, from the HTTP request to the rows in the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OrderSliceTest.java&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Testcontainers&lt;/span&gt;
&lt;span class="nd"&gt;@SpringBootTest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;OrderController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="nd"&gt;@EntityScan&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;basePackageClasses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@EnableJpaRepositories&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;basePackageClasses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@ImportAutoConfiguration&lt;/span&gt;&lt;span class="o"&gt;({&lt;/span&gt;
        &lt;span class="nc"&gt;DataSourceAutoConfiguration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;HibernateJpaAutoConfiguration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;TransactionAutoConfiguration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;WebMvcAutoConfiguration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="nd"&gt;@AutoConfigureMockMvc&lt;/span&gt;
&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderSliceTest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Container&lt;/span&gt;
    &lt;span class="nd"&gt;@ServiceConnection&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;PostgreSQLContainer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostgreSQLContainer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"postgres:17"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="nc"&gt;MockMvcTester&lt;/span&gt; &lt;span class="n"&gt;mvc&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@MockitoBean&lt;/span&gt;
    &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;placesOrder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mvc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;OK&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByCustomerId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42L&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;hasSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;rejectsOrderWithMoreThanTenItems&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mvc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BAD_REQUEST&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByCustomerId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42L&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;verifyNoInteractions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; Naming the classes is what keeps this a slice. The context holds the controller and the&lt;br&gt;
service of one feature, and nothing else in the application is loaded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; Naming them also turns off component scanning, so the entity has to be pointed at by hand.&lt;br&gt;
Leave this out and Hibernate starts with no mapped types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.&lt;/strong&gt; Same for the repository, which is an interface that something has to generate an&lt;br&gt;
implementation for.&lt;/p&gt;

&lt;p&gt;The four autoconfigurations below are the rest of what this slice needs. Listing them is the price&lt;br&gt;
of not booting the application, and the list ends up being a fair description of the feature's real&lt;br&gt;
dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.&lt;/strong&gt; The one stub in the test, because &lt;code&gt;PaymentGateway&lt;/code&gt; is the one dependency that leaves the&lt;br&gt;
feature. Everything else in the slice runs for real.&lt;/p&gt;

&lt;p&gt;One test class, and the request travels the entire slice. JSON deserialisation, validation, the&lt;br&gt;
controller, the service, the transaction, Hibernate, and a real Postgres. The assertion afterwards&lt;br&gt;
reads the rows back, so nothing along that path is assumed.&lt;/p&gt;

&lt;p&gt;Stub at the edges of the slice, never inside it. A stubbed repository would have left the SQL and&lt;br&gt;
the mapping untested, which is most of what actually goes wrong, and running the real thing is what&lt;br&gt;
lets the second test assert that a rejected order leaves the database untouched. That is a claim&lt;br&gt;
about the feature end to end rather than about any class inside it.&lt;/p&gt;

&lt;p&gt;Nothing from &lt;code&gt;customer&lt;/code&gt; or &lt;code&gt;payment&lt;/code&gt; is in the context, so a change in either cannot break this&lt;br&gt;
test. That containment is also why one test per feature stays practical as the application grows.&lt;br&gt;
The context is a function of the slice, not of the codebase, so the tenth feature does not slow down&lt;br&gt;
the first feature's test.&lt;/p&gt;

&lt;h2&gt;
  
  
  When layers are fine
&lt;/h2&gt;

&lt;p&gt;If every feature in the application has the same shape, layers cost nothing and give each kind of&lt;br&gt;
file one obvious home. A CRUD service over a dozen tables does not need any of this.&lt;/p&gt;

&lt;p&gt;Slices start paying when features stop resembling each other, and when parts of the system change&lt;br&gt;
at different rates. The signal is practical rather than architectural. If implementing one feature&lt;br&gt;
means opening ten packages, the structure is organised around the framework. If everything you need&lt;br&gt;
to understand it sits under &lt;code&gt;order/&lt;/code&gt;, it is organised around the work.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>architecture</category>
      <category>testing</category>
    </item>
    <item>
      <title>Conditional decorator beans in Spring</title>
      <dc:creator>Dominik Kovács</dc:creator>
      <pubDate>Thu, 20 Aug 2026 23:19:00 +0000</pubDate>
      <link>https://dev.to/solodev-sk/conditional-decorator-beans-in-spring-581l</link>
      <guid>https://dev.to/solodev-sk/conditional-decorator-beans-in-spring-581l</guid>
      <description>&lt;h2&gt;
  
  
  The flag nobody should have to remember
&lt;/h2&gt;

&lt;p&gt;Address verification APIs bill per lookup. So do identity screening, document extraction, and most&lt;br&gt;
other useful third-party services. They also tend to offer a sandbox mode that returns plausible&lt;br&gt;
data for free, which is what you want everywhere except production.&lt;/p&gt;

&lt;p&gt;The obvious way to wire that up is a check where the call happens:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;don't do this&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isSimulate&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withSandbox&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. It also means the flag is now part of your business logic, and every future call site is&lt;br&gt;
one more place to forget it. The place that forgets is the one that bills you from a CI run.&lt;/p&gt;
&lt;h2&gt;
  
  
  A bean that wraps its own interface
&lt;/h2&gt;

&lt;p&gt;Put the flag in a bean instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SandboxAddressClient.java&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Primary&lt;/span&gt;
&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="nd"&gt;@ConditionalOnBooleanProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"address.simulate"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SandboxAddressClient&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;AddressClient&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AddressClient&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nc"&gt;SandboxAddressClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AddressClient&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delegate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;AddressResult&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AddressRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withSandbox&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; &lt;code&gt;@Primary&lt;/code&gt; is what keeps this invisible. Callers still ask for &lt;code&gt;AddressClient&lt;/code&gt; and now get&lt;br&gt;
the wrapper, so you don't touch a single call site and new callers inherit the behaviour without&lt;br&gt;
knowing it exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; &lt;code&gt;@ConditionalOnBooleanProperty&lt;/code&gt; does more than switch behaviour off. When the property is&lt;br&gt;
false the bean is never created, so there is nothing in the context to misconfigure and no branch&lt;br&gt;
to trace at runtime. Either the decorator is there or it isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.&lt;/strong&gt; The undecorated client, injected into its own decorator, which is the part that looks&lt;br&gt;
impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is not a circular dependency
&lt;/h2&gt;

&lt;p&gt;A class that implements &lt;code&gt;AddressClient&lt;/code&gt; and injects &lt;code&gt;AddressClient&lt;/code&gt; looks like it should blow up on&lt;br&gt;
startup. It would, if it were the only implementation.&lt;/p&gt;

&lt;p&gt;What saves it is that &lt;code&gt;@Primary&lt;/code&gt; applies to other beans asking for the type, not to the bean's own&lt;br&gt;
constructor. Spring needs an &lt;code&gt;AddressClient&lt;/code&gt; to build &lt;code&gt;SandboxAddressClient&lt;/code&gt;, leaves out the bean&lt;br&gt;
it's currently constructing, and finds one remaining candidate. That's the real client. Without that&lt;br&gt;
exclusion you'd be adding qualifiers and the two beans would need to know about each other.&lt;/p&gt;

&lt;p&gt;The other thing worth noticing is that the decorator doesn't fake anything. The request still&lt;br&gt;
serializes, authenticates, and comes back through the same error handling as production. Only the&lt;br&gt;
sandbox flag changes. Swap in a mock and you skip all of it, which is usually where the bugs live.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else this fits
&lt;/h2&gt;

&lt;p&gt;Any metered API with a sandbox works the same way. So does disarming something with a real-world&lt;br&gt;
effect, like notifications or webhook deliveries, where you want staging to go through the motions&lt;br&gt;
without anyone receiving anything.&lt;/p&gt;

&lt;p&gt;It also works for behaviour you only want in some deployments. Cache a read-heavy client in&lt;br&gt;
production but not locally, so you see fresh data while developing. Mask personal data in&lt;br&gt;
environments where a lot of people have access, and leave it alone where the access is legitimate.&lt;/p&gt;

&lt;p&gt;The pattern holds as long as the decision comes from where the application runs. If it depends on&lt;br&gt;
anything in the request, this is the wrong tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the indirection stops paying
&lt;/h2&gt;

&lt;p&gt;As soon as the wrapper starts deciding things per request, which lookups get sandboxed or which&lt;br&gt;
users get masked, put the flag in the request and be explicit about it. Request-dependent logic&lt;br&gt;
hidden behind &lt;code&gt;@Primary&lt;/code&gt; is the kind of cleverness people curse at later.&lt;/p&gt;

&lt;p&gt;There is a genuine cost even in the simple case. Someone debugging a call site sees an interface and&lt;br&gt;
gets no hint that a decorator is in the way. That's a fine trade when the wrapper does one&lt;br&gt;
environment-shaped thing you can explain in a sentence. It stops being fine once the decorator has&lt;br&gt;
business rules of its own, because nobody reading the caller has any reason to look for it.&lt;/p&gt;

&lt;p&gt;You can stack these, but only one bean can be &lt;code&gt;@Primary&lt;/code&gt;. Past that you're ordering a chain by hand,&lt;br&gt;
and at that point a configuration class that builds the stack in plain sight beats spreading it&lt;br&gt;
across annotations.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>designpatterns</category>
    </item>
  </channel>
</rss>
