Spring Boot for Dummies: Part 2.3 [Profiles & Persistence]

Yash Patel
11 min readNov 26, 2023

--

In this part we will see how to setup spring profiles and restructure some code to support a little complex data for persistence. Here is link to part 2.2

# Spring Profiles (Separating Configs)

Not sure if this is the right place to introduce spring profiles, it should have been in Part 1, but didn’t have an example of why it is needed.

Spring profiles are a feature in the Spring Framework that allows you to define and manage different configurations for your application. With profiles, you can create different sets of beans and configurations for different environments or scenarios, such as development, testing, or production.

It provides:

  1. Environment-specific configuration: it allows you to define specific configurations for different environments. For example, you can have separate database configurations for development, testing, and production environments. This helps ensure that your application behaves correctly in each environment.
  2. Dependency management: profiles enable you to define different sets of dependencies depending on the profile. For example, you may want to use an in-memory database for development and testing, but a production-grade database for the production environment. Profiles allow you to easily switch between different dependencies based on the active profile.
  3. Feature toggling: spring profiles can be used for feature toggling, where you enable or disable certain features of your application based on the active profile. This can be useful when you want to test new features in a specific environment before rolling them out to production.
  4. Testing: Spring profiles are particularly useful for testing purposes. You can define separate configurations for unit tests, integration tests, or end-to-end tests. This allows you to customize the behavior of your application during different types of testing and ensures that tests run consistently across different environments.

By using spring profiles, you can easily manage different configurations and adapt your application to different environments without modifying the code. It provides flexibility and maintainability, making it easier to deploy and manage your application in various scenarios.

So how do we make and use profiles, it’s very easy. We just need to create a file named [application-dev.properties](<http://application-dev.properties>) for the dev environment, [application-staging.properties](<http://application-staging.properties>) for staging, and so on. Just name the file as application-<profile name>.properties to. create a config file that will be used for that profile only.

Let’s create a dev, staging, and prod profile, for the dev profile we can use an In Memory H2 database, for staging we can use our Postgres Docker Container and for Prod, we will add a connection to a remote database on Azure.

Creating application-dev.properties


# DB Connection
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

Note: Make sure to add the dependency for H2 in your pom.xml file.

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

Creating [application-staging.properties](<http://application-staging.properties>) (Connect to PostgreSQL)

spring.datasource.url=jdbc:postgresql://localhost:5432/simplytodo
spring.datasource.username=admin
spring.datasource.password=mypassword
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update

Using application.properties is perfectly fine, but I personally like the structure that YML provides. It's easy to understand and somewhat pretty to look at. (It's a matter of personal choice)

Spring can also read from file like application.yml and similarly from application-dev.yml , application-prod.yml and so on. Let’s convert our app to use yml configs.

You can go to this website Online Properties to YAML Converter (javainuse.com) and paste the code in .proerties file for it to generate yml code. Copy this code to respective yml files.

we put the yml files in same directory as [application.properties](<http://application.properties>), once we all yml files set up you can delete the application.properties file.

For example, dev profile’s application-dev.yml will look like.

spring:
datasource:
driverClassName: org.h2.Driver
password: password
url: jdbc:h2:mem:testdb
username: sa
jpa:
database-platform: org.hibernate.dialect.H2Dialect

make sure to also create application.yml and put the common code there.

For example, the actuator code (let’s assume) is common to all profiles. We paste that in the application.yml file.

management:
endpoints:
web:
base-path: /actuator
exposure:
include: '*'

Note: When running a profile, spring tries to read the value from the provided profile (let’s say dev) but if it cannot find any property in application-dev.yml it will fall back on application.yml . So you keep all common stuff in application.yml and only profile specific config in application-profilename.yml

Now, if you try to run the application It Fails, because we have not set a default profile for spring to fall back on.

Let’s make the dev profile as default profile, by adding the following code in application.yml

spring:
profiles:
active: dev

Running the app now shows that dev profile is active.

This will still fail. Because we would not be able to construct TodoDAO.java as we were connecting to Postgres database and our dev is connecting to H2 DB, so, this would fail.

So, what we do now? We can use springs @Conditional annotation to only initialize the bean if we find the property spring.datasource.driverClassName to be equal to org.postgresql.Driver

The @Conditional annotation in Java is used to conditionally initialize a bean or a component based on certain conditions. It allows you to define custom conditions to control the creation of beans in your application context.

There are different ways to use @Conditional annotation in Java:

  1. Using a Condition Class: You can create a custom Condition class that implements the Condition interface provided by Spring Framework. The matches() method of the Condition interface should return true or false based on the conditions you define. You can then use the @Conditional annotation on a bean declaration and specify your custom condition class as the value.
  2. Using Standard Conditions: Spring Framework provides a set of standard conditions that you can use directly with the @Conditional annotation. For example, you can use @ConditionalOnProperty to conditionally initialize a bean based on the value of a specific property in the application configuration. For our case we can use @ConditionalOnProperty to match driver class name with Postgres driver.
@Component
@ConditionalOnProperty(name = "spring.datasource.driverClassName", havingValue = "org.postgresql.Driver")
public class TodoTaskDao{
// rest of code
}

We can also use the @Profile annotation in Spring is used to specify which beans should be initialized based on the active profiles in your application. By using @Profile, you can define beans that are specific to certain profiles and ensure that they are only initialized when the corresponding profile is active. You can annotate a class or method with @Profile and specify the profile names as the value.

To use @Profile annotation to initialize classes specific to a Spring profile by annotating the class or method with @Profile and specify the profile names as the value. make sure to have the profiles defaulted to one of the profiles or set:

-Dspring.profiles.active=profile-name while running app or add to VM args while running on IntelliJ IDEA.

Using Staging profile

For example, if you have a class MyService that should only be initialized when the "dev" profile is active, you can annotate it as follows:

@Profile("dev")
@Service
public class MyService {
// ...
} // this service will only be initialized for dev profilej

This will ensure that the MyService bean is only initialized when the "dev" profile is active.

These annotations provide powerful ways to conditionally initialize beans and control the behavior of your Spring application based on different conditions and profiles.

Note: One thing to wonder is what happens to all @Autowired instances of MyService, because the bean is never created, we can’t auto-wire it directly. We need to change all instances with.

@Autowired(required = false)
private MyService myService;

But this will make it null so if you attempt to use it, it throws null pointer exception. We can either handle the exception at all places we use myService and check for null or we can have the exception handler, we can use reflection there to output the proper message. (I like the later). I will leave it up to you to implement. If any help needed, please add a comment.

# Building Strategy

Till now we worked with simple objects and types.

Let’s complicate things a little bit (I hate it too 😂). We had a simple TodoTask. We now evolve the task to be a little more complex and structured.

As shown in the image above, we need to make the description part able to hold text, code, images etc. So, we need a generic solution for that. Moreover, the structure of our TodoTask is no longer fixed and can vary with task and user, we need a better way to represent this loose structure. we also need some storage solution to store images and any other files that may be uploaded by the user in the description.

Approach!

To store images, we can use Amazon S3, to store this loose TodoTask object we can use MongoDB as it uses a JSON-like format to store documents, which directly maps to native objects in most modern programming languages, making it a natural choice. MongoDB is highly versatile and does not require creating tables when working with it, which adds a great advantage when storing big and uncategorized data.

Note: Needless to say, there may be better solutions and choice of DB to this problem. But for our example and app, I think this should suffice.

Let’s change the code in TodoTask to work with our SQL Database, then we will change it to MongoDB. This way you will also see that SQL is capable of doing things, but Mongo is a better choice.

Note: You may need to comment come code in class TodoTaskDAO and also mark it @Depricated it's a good practice.

Updating some classes:


@Data
@Entity
public class TodoTask {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private UUID uuid = UUID.randomUUID();

@NotEmpty(message = "Title cannot be null")
private String title;

/*
One task can only have one root node for DescriptionBlock
@JoinColumn tells us that the name "description_id" will be used for this relation.
*/
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "description_id")
private DescriptionBlock description; // description for this task, root node.

private TodoTaskStatus status = TodoTaskStatus.NOT_STARTED; // default value

@FutureOrPresent(message = "Due date cannot be in the past")
private Date dueDate; // due date for this task

/*
One Object can be associated with only one Metadata Object
*/
@OneToOne(cascade = CascadeType.ALL)
Metadata metadata = new Metadata(); // metadata for this task

private Set<String> tags; // set of tags for this task

@ManyToOne
@JoinColumn(name = "user_id")
@JsonBackReference
private User user; // user who owns this task
}



// ------------------------- Description Block ---------------

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class DescriptionBlock {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private DescriptionBlockType blockType;
private String content;


@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "parent_id")
List<DescriptionBlock> childBlocks;

// if this method comes before other methods , we can infer block type
// if this is defined after, blockType will not be set.
public void setBlockType(DescriptionBlockType blockType) {
this.blockType = blockType;
}

public void setContent(String content) {
if(this.blockType == null)
throw new IllegalStateException("Block type must be set before setting content");
this.content = content;
}

public void setChildBlocks(List<DescriptionBlock> childBlocks) {
if(this.blockType == null)
throw new IllegalStateException("Block type must be set before setting child blocks");
if(this.blockType == DescriptionBlockType.IMAGE)
throw new IllegalStateException("image block cannot have child blocks");
this.childBlocks = childBlocks;
}

public void addBlock(DescriptionBlockType blockType, String content) {
if(this.blockType == null)
throw new IllegalStateException("Block type must be set before adding child blocks");
this.childBlocks.add(DescriptionBlock.builder().blockType(blockType).content(content).build());
}
}

// -------------------- Metadata -------------------
@Entity
@Data
public class Metadata<T> {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private Date createdAt = Date.from(java.time.Instant.now());

@ManyToOne
@JoinColumn(name = "created_by")
private User createdBy;

private Date modifiedAt = Date.from(java.time.Instant.now());

private String objectType;
}

// -------------------- DescriptionBlockType ---------------------------
public enum DescriptionBlockType {
PLAIN_TEXT("PLAIN_TEXT"),
MD_FORMATTED("MD_FORMATTED"),
IMAGE("IMAGE");
private String value;
private DescriptionBlockType(String name) {
this.value = name;
}
public String getValue() {
return value;
}
}

Here we make field description of type DescriptionBlock and create a new class DescriptionBlock that can represent nested blocks of any type of combination. If we have a block of type “IMAGE” it can’t have sub-blocks (I guess that makes sense).

🤔 Reminder: CascadeType.All means operations performed on a parent entity should be propagated to its child entities.

If you have questions about relations and annotations mentioned above, please highlight and comment here. 🙂

Note: please read comments in code to get full picture. It has some important information about behavior of code.

Now let’s update our service TodoServiceto make this logic work.

@Service
public class TodoService {

@Autowired
private User loggedUser;

@Autowired(required = false)
private TodoTaskDao todoTaskDao;

@Autowired
private TodoTaskRepository todoTaskRepository;

public TodoTask createOrUpdate(TodoTask todoTask) throws TodoException {

Metadata<User> metadata = new Metadata<>();
metadata.setCreatedBy(loggedUser);
metadata.setObjectType(User.class.getTypeName());

todoTask.setMetadata(metadata);
todoTask.setUser(loggedUser);
return todoTaskRepository.saveAndFlush(todoTask);
}
// rest is same......
}

To create a logged user @Bean we just add it to our CustomBeanConfig . Or create a new one. This is to mimic logged user behaviour for now. We will work with actual logged user when we study spring security in part 4.

@Configuration
public class CustomBeanConfig
{
@Bean
public MessageDigest sha256(byte[] input) throws NoSuchAlgorithmException {
return MessageDigest.getInstance("SHA-256");
}

// set anything really, just make sure this exist in DB, if not create a user
/* Or you can inject User repository here and check
if user with Id 1 exist if not then create one.
Whatever suits you.
*/
@Bean
public User loggedUser() {
User user = new User();
user.setId(1L);
user.setName("Yash");
user.setPassword("password"); // Dont worry about password encryption, will cover all in part 4
return user;
}
}

Let’s create a new TodoTask with this somewhat complex structure.

Create Todo task

here is the Json response:

{
"id": 1,
"uuid": "be107957-5d44-4466-b983-3d2ee52a1b2e",
"title": "This new and updated task",
"description": {
"id": 1,
"blockType": "PLAIN_TEXT",
"content": "This is plain text description of new task",
"childBlocks": [
{
"id": 2,
"blockType": "MD_FORMATTED",
"content": "#This is a heading \n This is its description",
"childBlocks": [
{
"id": 3,
"blockType": "IMAGE",
"content": "https://th.bing.com/th/id/R.abb691e5453eda2fc5a9617bc7f07091?rik=zyzecjkT%2bXRqCA&pid=ImgRaw&r=0",
"childBlocks": null
}
]
},
{
"id": 4,
"blockType": "IMAGE",
"content": "https://th.bing.com/th/id/R.e5f7d4befcd9e717566c7271795ac189?rik=5D%2fjAfEuYEaZdw&riu=http%3a%2f%2fwww.pngall.com%2fwp-content%2fuploads%2f2016%2f03%2fSnoopy-Cartoon-PNG.png&ehk=QvBL8wC%2feLVIooFmMbZ%2f6GBsbIuRCBcC9yRzcvn3ckA%3d&risl=&pid=ImgRaw&r=0",
"childBlocks": null
}
]
},
"status": "NOT_STARTED",
"dueDate": null,
"metadata": {
"id": 1,
"createdAt": "2023-11-26T18:09:17.124+00:00",
"createdBy": {
"name": "Yash",
"email": null,
"phone": null
},
"modifiedAt": "2023-11-26T18:09:17.124+00:00",
"objectType": "com.simplytodo.entity.User"
},
"tags": [
"NewTask",
"FirstTask"
]
}

Note: the save result may say email and phone are null, but if you do HTTP GET on tasks, it will come fine. Because we are pulling from DB. We will fix this as we go further.

Your DB should look like

If you are wondering about reminders and notification function from image. Good thing, you are paying attention. it is not technically a part of prescience module, and we will add this feature a little later in the series.

In next module we will connect with MongoDB on cloud and store the TodoTask into it. Also, we will see how to store images and files with Amazon S3.

If you have any questions, please feel free to add a comment or highlight, I will try to reply as soon as possible. Thank you. Happy Coding :)

Here is link to part 2.4

--

--

Yash Patel

Software Developer. Extremely Curious | Often Wrong | Always Learning.