{"id":55362,"date":"2026-06-10T14:29:25","date_gmt":"2026-06-10T21:29:25","guid":{"rendered":"https:\/\/www.griddb.net\/?p=55362"},"modified":"2026-06-17T14:29:47","modified_gmt":"2026-06-17T21:29:47","slug":"building-a-modern-job-board-with-spring-boot-griddb-cloud","status":"publish","type":"post","link":"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/","title":{"rendered":"Building a Modern Job Board with Spring Boot &#038; GridDB Cloud"},"content":{"rendered":"<p>\nIn this tutorial, we&#8217;ll build a fully functional job board web application from the ground up. Our application will allow users to browse available positions, search for jobs based on specific skills, and administrators can manage job listings. We&#8217;ll be working with three powerful technologies: <strong>Spring Boot<\/strong> to handle our backend component, <em>Thymeleaf<\/em> for creating dynamic web pages, and <strong>GridDB Cloud<\/strong> as our scalable database solution. As an exciting bonus, we&#8217;ll also integrate <strong>Spring AI<\/strong> with OpenAI&#8217;s language model to automatically generate relevant skill tags from job descriptions.\n<\/p>\n<p>\nThis project is designed to give you hands-on experience with real-world web development concepts. We&#8217;ll start with the basics, setting up our development environment and cofiguring our database connection, then gradually build up to more advanced features like search functionality and AI integration.\n<\/p>\n<p>\nBy the time we&#8217;re finished, you&#8217;ll have a complete understanding of how modern web applications work, from data storage and business logic to user interfaces and AI-powered features.\n<\/p>\n<h2 id=\"prerequisites-project-setup\">Prerequisites &#038; Project Setup<\/h2>\n<p>\nFirst, let&#8217;s make sure we have everything installed and configured properly before we start building our job board application.\n<\/p>\n<p><strong>Development Tools:<\/strong><\/p>\n<ul>\n<li><a href=\"https:\/\/jdk.java.net\/21\/\">Java 17 or later<\/a>, <a href=\"https:\/\/maven.apache.org\/download.cgi\">Maven 3.5+<\/a>, and your favorite IDE (<a href=\"https:\/\/spring.io\/guides\/gs\/intellij-idea\/\">IntelliJ IDEA<\/a> or <a href=\"https:\/\/code.visualstudio.com\/docs\/languages\/java\">VS Code<\/a>)<\/li>\n<li>A GridDB Cloud account. You can sign up for a GridDB Cloud Free instance at https:\/\/form.ict-toshiba.jp\/download_form_griddb_cloud_freeplan_e<\/li>\n<li>An OpenAI API account for the AI-powered skill generation feature. You can find your Secret API key on the API key page.<\/li>\n<\/ul>\n<p>\nAfter completing the prerequisites, we&#8217;ll create a new Spring Boot application using <a href=\"https:\/\/start.spring.io\/\">Spring Initializr<\/a>. Here&#8217;s how we&#8217;ll set it up:\n<\/p>\n<ol>\n<li>Navigate to <a href=\"https:\/\/start.spring.io\/\">start.spring.io<\/a><\/li>\n<li>Configure your project:\n<ul>\n<li><strong>Project<\/strong>: Maven<\/li>\n<li><strong>Language<\/strong>: Java<\/li>\n<li><strong>Spring Boot<\/strong>: 3.5.x (latest stable version)<\/li>\n<li><strong>Group<\/strong>: com.example<\/li>\n<li><strong>Artifact<\/strong>: springboot-jobboard<\/li>\n<li><strong>Java Version<\/strong>: 17 or later<\/li>\n<\/ul>\n<\/li>\n<li>Add the following dependencies:\n<ul>\n<li><strong>Spring Web<\/strong> &#8211; for creating our REST controllers and web layer<\/li>\n<li><strong>Thymeleaf<\/strong> &#8211; for server-side template rendering<\/li>\n<li><strong>Spring Security<\/strong> &#8211; for basic authentication (we&#8217;ll keep it simple)<\/li>\n<\/ul>\n<\/li>\n<li>Click <strong>Generate<\/strong> to download a ZIP file with our project structure<\/li>\n<\/ol>\n<p>\nOnce you&#8217;ve downloaded and extracted the project, import it into your IDE. Then make sure we have the main project structure as follows:\n<\/p>\n<div class=\"clipboard\">\n<pre><code class=\"language-sh\">$ \u251c\u2500\u2500\u2500java\r\n$ \u2502   \u2514\u2500\u2500\u2500com\r\n$ \u2502       \u2514\u2500\u2500\u2500example\r\n$ \u2502           \u2514\u2500\u2500\u2500springbootjobboard\r\n$ \u2502               \u251c\u2500\u2500\u2500config\r\n$ \u2502               \u251c\u2500\u2500\u2500controller\r\n$ \u2502               \u251c\u2500\u2500\u2500domain\r\n$ \u2502               \u251c\u2500\u2500\u2500model\r\n$ \u2502               \u251c\u2500\u2500\u2500repos\r\n$ \u2502               \u251c\u2500\u2500\u2500rest\r\n$ \u2502               \u251c\u2500\u2500\u2500security\r\n$ \u2502               \u251c\u2500\u2500\u2500service\r\n$ \u2502               \u251c\u2500\u2500\u2500util\r\n$ \u2502               \u2514\u2500\u2500\u2500webapi\r\n$ \u2502                   \u2514\u2500\u2500\u2500acquisition<\/code><\/pre>\n<\/div>\n<p>\nWe&#8217;ll then add the additional dependencies we need for GridDB Cloud integration and AI-powered features.\n<\/p>\n<div class=\"clipboard\">\n<pre><code class=\"language-xml\">&lt;dependency&gt;\r\n   &lt;groupId&gt;org.springframework.ai&lt;\/groupId&gt;\r\n   &lt;artifactId&gt;spring-ai-starter-model-openai&lt;\/artifactId&gt;\r\n   &lt;version&gt;1.0.1&lt;\/version&gt;\r\n&lt;\/dependency&gt;\r\n&lt;dependency&gt;\r\n   &lt;groupId&gt;com.github.f4b6a3&lt;\/groupId&gt;\r\n   &lt;artifactId&gt;tsid-creator&lt;\/artifactId&gt;\r\n   &lt;version&gt;5.2.5&lt;\/version&gt;\r\n&lt;\/dependency&gt;\r\n&lt;dependency&gt;\r\n   &lt;groupId&gt;org.apache.commons&lt;\/groupId&gt;\r\n   &lt;artifactId&gt;commons-text&lt;\/artifactId&gt;\r\n   &lt;version&gt;1.14.0&lt;\/version&gt;\r\n&lt;\/dependency&gt;<\/code><\/pre>\n<\/div>\n<p>\n> :bulb: <strong>Tip<\/strong>: If you prefer to skip the setup process, you can clone the completed project repository <a href=\"https:\/\/github.com\/alifruliarso?tab=repositories\">here<\/a>.\n<\/p>\n<p>\nAfter adding all dependencies, next configure the application properties.\n<\/p>\n<ul>\n<li>GridDB Configuration<\/li>\n<\/ul>\n<div class=\"clipboard\">\n<pre><code class=\"language-txt\">griddbcloud.base-url=YOUR_GRIDDBCLOUD_BASE_URL\r\ngriddbcloud.auth-token=YOUR_GRIDDBCLOUD_AUTH_TOKEN<\/code><\/pre>\n<\/div>\n<ul>\n<li>OpenAI API Key<\/li>\n<\/ul>\n<div class=\"clipboard\">\n<pre><code class=\"language-txt\">spring.ai.openai.api-key=${OPENAI_API_KEY}<\/code><\/pre>\n<\/div>\n<p>\nThen <strong>Export<\/strong> your Open AI API keys as environment variables:\n<\/p>\n<div class=\"clipboard\">\n<pre><code class=\"language-sh\">$    export OPENAI_API_KEY=&quot;your_api_key_here&quot;\r\n   <\/code><\/pre>\n<\/div>\n<h2 id=\"database-integration\">Database Integration<\/h2>\n<p>\nTo access the GridDB Web API endpoint, we must provide an access token in the HTTP Authorization header. The access token is a <code>Base64<\/code> encoded string of the username and password, separated by a colon. To access the configured values above, we need to bind the properties defined in the <code>application.properties<\/code> file to a POJO class using the <code>@ConfigurationProperties<\/code> annotation.\n<\/p>\n<div class=\"clipboard\">\n<pre><code class=\"language-java\">\/\/ GridDbCloudClientProperties.java\r\n@Component\r\n@ConfigurationProperties(prefix = &quot;griddbcloud&quot;)\r\npublic class GridDbCloudClientProperties {\r\n    private String baseUrl;\r\n    private String authToken;\r\n    \/\/setter, getter \r\n}<\/code><\/pre>\n<\/div>\n<p>\nNext, we create <code>GridDbCloudClient<\/code> under <code>webapi<\/code> package, a centralized place to construct all HTTP requests to the GridDB Cloud Web API.\n<\/p>\n<div class=\"clipboard\">\n<pre><code class=\"language-java\">\/\/ GridDbCloudClient.java\r\npublic class GridDbCloudClient {\r\n    private final RestClient restClient;\r\n\r\n    public GridDbCloudClient(String baseUrl, String authToken) {\r\n        this.restClient =\r\n            RestClient.builder()\r\n                    .baseUrl(baseUrl)\r\n                    .defaultHeader(&quot;Authorization&quot;, &quot;Basic &quot; + authToken)\r\n                    .defaultHeader(&quot;Content-Type&quot;, &quot;application\/json&quot;)\r\n                    .defaultHeader(&quot;Accept&quot;, &quot;application\/json&quot;)\r\n                    .build();\r\n    }\r\n\r\n    public void createContainer(GridDbContainerDefinition containerDefinition) {\r\n        restClient\r\n            .post()\r\n            .uri(&quot;\/containers&quot;)\r\n            .body(containerDefinition)\r\n            .retrieve()\r\n            .toBodilessEntity();\r\n    }\r\n}<\/code><\/pre>\n<\/div>\n<ul>\n<li>The <code>org.springframework.web.client.RestClient<\/code> is built and configured only once during application startup, and the same instance is reused.<\/li>\n<li>Use <code>baseUrl()<\/code> to set the common base URL for all requests made to the GridDB Cloud Web API.<\/li>\n<li>Configure the <code>Authorization<\/code> header that should be included in every request by default using <code>defaultHeader()<\/code>.<\/li>\n<li>The <code>Accept<\/code> HTTP request header tells the server that our client wants to receive a <code>JSON<\/code> content in the response.<\/li>\n<li>The <code>Content-Type<\/code> header tells the server that <code>JSON<\/code> data is being sent in the request body.<\/li>\n<\/ul>\n<p>\nNext, add a helper method for adding rows to the specified container.\n<\/p>\n<div class=\"clipboard\">\n<pre><code class=\"language-java\">\/\/ GridDbCloudClient.java\r\npublic void registerRows(String containerName, Object body) {\r\n    ResponseEntity&lt;String&gt; result =\r\n        restClient\r\n            .put()\r\n            .uri(&quot;\/containers\/&quot; + containerName + &quot;\/rows&quot;)\r\n            .body(body)\r\n            .retrieve()\r\n            .toEntity(String.class);\r\n}<\/code><\/pre>\n<\/div>\n<ul>\n<li>The <code>registerRows<\/code> method: insert or update multiple rows of data in a specific GridDB container through the Web API. It takes the container&#8217;s name and the rows to be registered as parameters.<\/li>\n<li><code>.body(body)<\/code> we provide the Java object that will be automatically converted to JSON by Spring&#8217;s message converter.<\/li>\n<\/ul>\n<p>\nNext, we need to create a method to execute an SQL statement that combines rows from one or multiple tables. For example, search job postings by skill, need to join the table JobPost with Skill.\n<\/p>\n<p>\nThe GridDB Web API endpoint executes one or more SQL SELECT statements on a specific database:\n<\/p>\n<ul>\n<li>URL: <code>\/:cluster\/dbs\/:database\/sql\/dml\/query<\/code><\/li>\n<li>HTTP Method: <code>Post<\/code><\/li>\n<li>Example request body:<\/li>\n<\/ul>\n<div class=\"clipboard\">\n<pre><code class=\"language-json\">    [\r\n        {&quot;stmt&quot; : &quot;select * from container1&quot;},\r\n        {&quot;stmt&quot; : &quot;select * from myTable&quot;}\r\n    ]\r\n    <\/code><\/pre>\n<\/div>\n<p>\nHere is the helper method:\n<\/p>\n<div class=\"clipboard\">\n<pre><code class=\"language-java\">public SQLSelectResponse[] select(List&lt;GridDbCloudSQLStmt&gt; sqlStmts) {\r\n    try {\r\n        ResponseEntity&lt;SQLSelectResponse[]&gt; responseEntity =\r\n            restClient\r\n                .post()\r\n                .uri(&quot;\/sql\/dml\/query&quot;)\r\n                .body(sqlStmts)\r\n                .retrieve()\r\n                .toEntity(SQLSelectResponse[].class);\r\n        return responseEntity.getBody();\r\n    } catch (Exception e) {\r\n        throw new GridDbException(&quot;Failed to execute \/sql\/dml\/query&quot;,HttpStatusCode.valueOf(500),e.getMessage(),e);\r\n    }\r\n}<\/code><\/pre>\n<\/div>\n<h3 id=\"core-data-model\">Core Data Model<\/h3>\n<p>\nIn a job board platform, the schema would include tables like Company, JobPost, JobPostSkill, SkillTag, and Users. The schema would facilitate efficient storage and retrieval of job postings, company information, job skills tags, and user roles.\n<\/p>\n<p><a href=\"\/wp-content\/uploads\/2026\/06\/dbdiagram_jobboard-scaled.png\"><img fetchpriority=\"high\" decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/06\/dbdiagram_jobboard-scaled.png\" alt=\"\" width=\"2560\" height=\"1434\" class=\"aligncenter size-full wp-image-55364\" srcset=\"\/wp-content\/uploads\/2026\/06\/dbdiagram_jobboard-scaled.png 2560w, \/wp-content\/uploads\/2026\/06\/dbdiagram_jobboard-300x168.png 300w, \/wp-content\/uploads\/2026\/06\/dbdiagram_jobboard-1024x574.png 1024w, \/wp-content\/uploads\/2026\/06\/dbdiagram_jobboard-768x430.png 768w, \/wp-content\/uploads\/2026\/06\/dbdiagram_jobboard-1536x860.png 1536w, \/wp-content\/uploads\/2026\/06\/dbdiagram_jobboard-2048x1147.png 2048w, \/wp-content\/uploads\/2026\/06\/dbdiagram_jobboard-150x85.png 150w, \/wp-content\/uploads\/2026\/06\/dbdiagram_jobboard-600x336.png 600w\" sizes=\"(max-width: 2560px) 100vw, 2560px\" \/><\/a><\/p>\n<ul>\n<li><code>User<\/code> has 3 roles: RECRUITER, ADMIN, and APPLICANT.<\/li>\n<li>Job post types are: FULL_TIME(&#8220;Full Time&#8221;), PART_TIME(&#8220;Part Time&#8221;), CONTRACT(&#8220;Contract&#8221;), INTERNSHIP(&#8220;Internship&#8221;)<\/li>\n<li>Work models are: ONSITE, HYBRID, and REMOTE.<\/li>\n<li>Each job post can have multiple skills.<\/li>\n<\/ul>\n<p>\nThis database design should support the process of creating and searching jobs in general. We will build our application based on this design. Let&#8217;s start with our primary data in a job board.\n<\/p>\n<h4 id=\"job-post\">Job Post<\/h4>\n<p>\nThe <code>job_post<\/code> table is the most important data model, representing a single job listing.\n<\/p>\n<p>\nWe need to create a class to centralize the database operations from creating tables, querying rows, and creating or updating rows. Here is our container class:\n<\/p>\n<details open>\n<summary>service\/JobPostContainer.java<\/summary>\n<div class=\"clipboard\">\n<pre><code class=\"language-java\">@Component\r\npublic class JobPostContainer {\r\n    private final Logger log = LoggerFactory.getLogger(getClass());\r\n    private final GridDbCloudClient gridDbCloudClient;\r\n    private static final String TBL_NAME = &quot;JBJobPost&quot;;\r\n\r\n    public JobPostContainer(GridDbCloudClient gridDbCloudClient) {\r\n        this.gridDbCloudClient = gridDbCloudClient;\r\n    }\r\n\r\n    public void createTable() {\r\n        List&lt;GridDbColumn&gt; columns =\r\n                List.of(\r\n                        new GridDbColumn(&quot;id&quot;, &quot;STRING&quot;, Set.of(&quot;TREE&quot;)),\r\n                        new GridDbColumn(&quot;title&quot;, &quot;STRING&quot;),\r\n                        new GridDbColumn(&quot;description&quot;, &quot;STRING&quot;),\r\n                        new GridDbColumn(&quot;jobType&quot;, &quot;STRING&quot;, Set.of(&quot;TREE&quot;)),\r\n                        new GridDbColumn(&quot;maximumMonthlySalary&quot;, &quot;DOUBLE&quot;),\r\n                        new GridDbColumn(&quot;datePosted&quot;, &quot;TIMESTAMP&quot;),\r\n                        new GridDbColumn(&quot;companyId&quot;, &quot;STRING&quot;, Set.of(&quot;TREE&quot;)),\r\n                        new GridDbColumn(&quot;workModel&quot;, &quot;STRING&quot;, Set.of(&quot;TREE&quot;)),\r\n                        new GridDbColumn(&quot;location&quot;, &quot;STRING&quot;),\r\n                        new GridDbColumn(&quot;applyUrl&quot;, &quot;STRING&quot;));\r\n\r\n        GridDbContainerDefinition containerDefinition =\r\n                GridDbContainerDefinition.build(TBL_NAME, columns);\r\n        this.gridDbCloudClient.createContainer(containerDefinition);\r\n    }\r\n}<\/code><\/pre>\n<\/div>\n<ul>\n<li>Here, the <code>GridDbCloudClient<\/code> is being injected into <code>JobPostContainer<\/code> via the constructor. By using constructor injection, we get some advantages: preventing circular dependencies at compile time and easier to unit test by simply passing mock or stub implementations of dependencies directly to the constructor during testing.<\/li>\n<\/ul>\n<div class=\"clipboard\">\n<pre><code class=\"language-java\">public void saveRecords(List&lt;JobPostRecord&gt; jobPostRecords) {\r\n    StringBuilder sb = new StringBuilder();\r\n    sb.append(&quot;[&quot;);\r\n    for (int i = 0; i &lt; jobPostRecords.size(); i++) {\r\n        JobPostRecord record = jobPostRecords.get(i);\r\n        sb.append(&quot;[&quot;);\r\n        sb.append(&quot;\\&quot;&quot;).append(record.id()).append(&quot;\\&quot;&quot;);\r\n        sb.append(&quot;, &quot;);\r\n        sb.append(&quot;\\&quot;&quot;).append(StringEscapeUtils.escapeJson(record.title())).append(&quot;\\&quot;&quot;);\r\n        sb.append(&quot;, &quot;);\r\n        sb.append(&quot;\\&quot;&quot;).append(StringEscapeUtils.escapeJson(record.description())).append(&quot;\\&quot;&quot;);\r\n        sb.append(&quot;, &quot;);\r\n        sb.append(&quot;\\&quot;&quot;).append(record.jobType().name()).append(&quot;\\&quot;&quot;);\r\n        sb.append(&quot;, &quot;);\r\n        sb.append(record.maximumMonthlySalary());\r\n        sb.append(&quot;, &quot;);\r\n        sb.append(&quot;\\&quot;&quot;)\r\n                .append(DateTimeUtil.formatToZoneDateTimeString(record.datePosted()))\r\n                .append(&quot;\\&quot;&quot;);\r\n        sb.append(&quot;, &quot;);\r\n        sb.append(&quot;\\&quot;&quot;).append(record.companyId()).append(&quot;\\&quot;&quot;);\r\n        sb.append(&quot;, &quot;);\r\n        sb.append(&quot;\\&quot;&quot;).append(record.workModel().name()).append(&quot;\\&quot;&quot;);\r\n        sb.append(&quot;, &quot;);\r\n        if (record.location() != null) {\r\n            sb.append(&quot;\\&quot;&quot;)\r\n                    .append(StringEscapeUtils.escapeJson(record.location()))\r\n                    .append(&quot;\\&quot;&quot;);\r\n        } else {\r\n            sb.append(&quot;null&quot;);\r\n        }\r\n        sb.append(&quot;, &quot;);\r\n        if (record.applyUrl() != null) {\r\n            sb.append(&quot;\\&quot;&quot;).append(record.applyUrl()).append(&quot;\\&quot;&quot;);\r\n        } else {\r\n            sb.append(&quot;null&quot;);\r\n        }\r\n        sb.append(&quot;]&quot;);\r\n        if (i &lt; jobPostRecords.size() - 1) {\r\n            sb.append(&quot;, &quot;);\r\n        }\r\n    }\r\n    sb.append(&quot;]&quot;);\r\n    String result = sb.toString();\r\n    this.gridDbCloudClient.registerRows(TBL_NAME, result);\r\n}<\/code><\/pre>\n<\/div>\n<ul>\n<li><code>saveRecords(List<JobPostRecord> jobPostRecords)<\/code>: converts a list of <code>JobPostRecord<\/code> objects into a JSON-formatted string array and saves it to the GridDB instance using <code>GridDbCloudClient<\/code>. For <code>datePosted<\/code>, we should convert it into a string as UTC time format like <code>YYYY-MM-DDThh:mm:ss.SSSZ<\/code>. We escape the character in a String to prevent JSON parsing errors.<\/li>\n<\/ul>\n<div class=\"clipboard\">\n<pre><code class=\"language-java\">    public List&lt;JobPostRecord&gt; getAll() {\r\n        AcquireRowsRequest requestBody =\r\n                AcquireRowsRequest.builder().limit(50L).sort(&quot;id ASC&quot;).build();\r\n        AcquireRowsResponse response = this.gridDbCloudClient.acquireRows(TBL_NAME, requestBody);\r\n        if (response == null || response.getRows() == null) {\r\n            log.error(&quot;Failed to acquire rows from GridDB&quot;);\r\n            return List.of();\r\n        }\r\n        List&lt;JobPostRecord&gt; jobPosts = convertResponseToRecord(response.getRows());\r\n        return jobPosts;\r\n    }\r\n\r\n    public List&lt;JobPostRecord&gt; searchBySkill(String skill) {\r\n        String stmt =\r\n        &quot;&quot;&quot;\r\n            SELECT jp.* \\\r\n            FROM JBJobPost jp \\\r\n            JOIN JBCompany c ON jp.companyId = c.id \\\r\n            JOIN JBJobPostSkill jps ON jp.id = jps.jobPostId \\\r\n            JOIN JBSkillTag st ON jps.skillTagId = st.id \\\r\n            WHERE LOWER(st.name) IN (&#x27;%s&#x27;) \\\r\n            GROUP BY jp.id, c.name\r\n        &quot;&quot;&quot;.formatted(skill.toLowerCase());\r\n\r\n        List&lt;GridDbCloudSQLStmt&gt; statementList = List.of(new GridDbCloudSQLStmt(stmt));\r\n        SQLSelectResponse[] response = this.gridDbCloudClient.select(statementList);\r\n        if (response == null || response.length != statementList.size()) {\r\n            \/\/ log.error(&quot;ERROR&quot;);\r\n            return List.of();\r\n        }\r\n\r\n        List&lt;List&lt;Object&gt;&gt; results = response[0].getResults();\r\n        if (results.isEmpty()) {\r\n            log.info(&quot;No result for searching skill: {}&quot;, skill);\r\n            return List.of();\r\n        }\r\n\r\n        List&lt;JobPostRecord&gt; records = convertResponseToRecord(results);\r\n        return records;\r\n    }<\/code><\/pre>\n<\/div>\n<ul>\n<li><code>getAll()<\/code>: retrieves all job post records from a GridDB database, converts them into <code>JobPostRecord<\/code> objects, and returns them as a List.<\/li>\n<li><code>searchBySkill(String skill)<\/code>: find job posts that require a specific skill. We create a SQL query using Java&#8217;s text block. The SQL query selects all columns from the JobPost table, joins with the Company, JobPostSkill, and SkillTag tables, then filters by skill name. Then wraps the SQL statement in a <code>GridDbCloudSQLStmt<\/code> object and sends it to the GridDB Cloud using the <code>select<\/code> method of the client.<\/li>\n<\/ul>\n<\/details>\n<h3 id=\"service-layer\">Service Layer<\/h3>\n<p>\nNext, we&#8217;ll add the service layer that sits between the Web Controller and the Data Access layer.\n<\/p>\n<div class=\"clipboard\">\n<pre><code class=\"language-java\">@Service\r\npublic class JobPostGridDbService {\r\n\r\n    private final JobPostContainer jobPostContainer;\r\n\r\n    public JobPostGridDbService(JobPostContainer jobPostContainer) {\r\n        this.jobPostContainer = jobPostContainer;\r\n    }\r\n\r\n    public static String nextId() {\r\n        return TsidCreator.getTsid().format(&quot;job_%s&quot;);\r\n    }\r\n\r\n    public List&lt;JobPostDTO&gt; findAll(String searchSkill) {\r\n        final List&lt;JobPostRecord&gt; jobPosts;\r\n        if (searchSkill != null &amp;&amp; !searchSkill.isBlank()) {\r\n            jobPosts = jobPostContainer.searchBySkill(searchSkill);\r\n        } else {\r\n            jobPosts = jobPostContainer.getAll();\r\n        }\r\n        return jobPosts.stream()\r\n                .map(jobPost -&gt; mapToDTO(jobPost, new JobPostDTO()))\r\n                .collect(Collectors.toList());\r\n    }\r\n    public String create(final JobPostDTO jobPostDTO) {\r\n        String id = (jobPostDTO.getId() != null) ? jobPostDTO.getId() : nextId();\r\n        JobPostRecord newJobPost =\r\n                new JobPostRecord(\r\n                        id,\r\n                        jobPostDTO.getTitle(),\r\n                        jobPostDTO.getDescription(),\r\n                        jobPostDTO.getJobType(),\r\n                        jobPostDTO.getMaximumMonthlySalary(),\r\n                        jobPostDTO.getDatePosted(),\r\n                        jobPostDTO.getCompanyId(),\r\n                        jobPostDTO.getWorkModel(),\r\n                        jobPostDTO.getLocation(),\r\n                        jobPostDTO.getApplyUrl());\r\n        jobPostContainer.saveRecords(List.of(newJobPost));\r\n        return id;\r\n    }\r\n}<\/code><\/pre>\n<\/div>\n<ul>\n<li>This <code>service<\/code> class hides the database operation<\/li>\n<li>After getting the query result, transform it into a <code>DTO<\/code> class.<\/li>\n<\/ul>\n<div class=\"clipboard\">\n<pre><code class=\"language-java\">@Service\r\npublic class JobPostSkillGridDbService {\r\n\r\n    private final JobPostSkillContainer jobPostSkillContainer;\r\n\r\n    public JobPostSkillGridDbService(JobPostSkillContainer jobPostSkillContainer) {\r\n        this.jobPostSkillContainer = jobPostSkillContainer;\r\n    }\r\n\r\n    public void replaceSkillsForJobPost(String jobPostId, List&lt;String&gt; skillTagIds) {\r\n        deleteByJobPostId(jobPostId);\r\n        if (!skillTagIds.isEmpty()) {\r\n            createSkillsForJobPost(jobPostId, skillTagIds);\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<\/div>\n<ul>\n<li><code>replaceSkillsForJobPost<\/code>: update the list of skills for a job. It first removes all skill associations for the current job, then creates new associations between the job post and each skill tag ID in the list.<\/li>\n<\/ul>\n<h2 id=\"web-controller\">Web Controller<\/h2>\n<p>\nNext, let&#8217;s add the web controller class. This layer is the entry point of our web application. It receives requests, coordinates with the service layer to fulfill the data requested, and ensures users get the responses they expect. We will try to keep the controller thin and focus on its core responsibilities.\n<\/p>\n<details open>\n<summary>controller\/JobPostController.java<\/summary>\n<div class=\"clipboard\">\n<pre><code class=\"language-java\">@Controller\r\n@RequestMapping(&quot;\/jobs&quot;)\r\npublic class JobPostController {\r\n    private final Logger log = LoggerFactory.getLogger(getClass());\r\n\r\n    private final JobPostGridDbService jobPostService;\r\n    private final CompanyGridDbService companyService;\r\n    private final JobPostSkillGridDbService jobPostSkillService;\r\n    private final SkillTagGridDbService skillTagService;\r\n    private final ChatModel chatModel;\r\n    private final TableSeeder tableSeeder;\r\n\r\n    private final Map&lt;String, String&gt; jobTypeValues =\r\n            Arrays.stream(JobPostType.values())\r\n                    .collect(\r\n                            java.util.stream.Collectors.toMap(\r\n                                    JobPostType::name, JobPostType::getLabel));\r\n\r\n    public JobPostController(\r\n            final JobPostGridDbService jobPostService,\r\n            final CompanyGridDbService companyService,\r\n            final JobPostSkillGridDbService jobPostSkillService,\r\n            final SkillTagGridDbService skillTagService,\r\n            ChatModel chatModel,\r\n            TableSeeder tableSeeder) {\r\n        this.jobPostService = jobPostService;\r\n        this.companyService = companyService;\r\n        this.jobPostSkillService = jobPostSkillService;\r\n        this.skillTagService = skillTagService;\r\n        this.chatModel = chatModel;\r\n        this.tableSeeder = tableSeeder;\r\n    }\r\n\r\n    @ModelAttribute\r\n    public void prepareContext(final Model model) {\r\n        Map&lt;String, String&gt; companies =\r\n                companyService.findAll().stream()\r\n                        .collect(\r\n                                java.util.stream.Collectors.toMap(\r\n                                        com -&gt; com.getId(), com -&gt; com.getName()));\r\n        model.addAttribute(&quot;jobTypeValues&quot;, jobTypeValues);\r\n        model.addAttribute(&quot;workModelValues&quot;, WorkModel.values());\r\n        model.addAttribute(&quot;companyIdValues&quot;, companies);\r\n    }\r\n\r\n    @GetMapping\r\n    public String list(\r\n            @RequestParam(name = &quot;searchSkill&quot;, required = false) String searchSkill,\r\n            final Model model) {\r\n        List&lt;JobPostDTO&gt; jobs = jobPostService.findAll(searchSkill);\r\n        List&lt;JobListingResponse&gt; jobPosts =\r\n                jobs.stream()\r\n                        .map(\r\n                                jobPost -&gt; {\r\n                                    JobListingResponse response =\r\n                                            buildJobPostResponse(jobPost.getId());\r\n                                    return response;\r\n                                })\r\n                        .toList();\r\n        model.addAttribute(&quot;jobPosts&quot;, jobPosts);\r\n        model.addAttribute(&quot;searchSkill&quot;, searchSkill);\r\n        return &quot;jobs\/list&quot;;\r\n    }\r\n}<\/code><\/pre>\n<\/div>\n<ul>\n<li>All dependencies (service, component) are injected using constructor injection.<\/li>\n<li>Annotated with <code>@ModelAttribute<\/code> the <code>prepareContext(final Model model)<\/code> will be executed before every controller method. It populates common attributes (job types, work models, companies), making it available to all Thymeleaf templates, useful for building dropdowns.<\/li>\n<\/ul>\n<\/details>\n<h2 id=\"frontend-using-thymeleaf\">Frontend using Thymeleaf<\/h2>\n<p>\nThymeleaf provides a flexible approach to render dynamic web pages in Spring Boot applications. We use fragments to create reusable template components and structure the templates in logical directories.\n<\/p>\n<div class=\"clipboard\">\n<pre><code class=\"language-sh\">$ src\/\r\n$ \u2514\u2500\u2500 main\/\r\n$     \u2514\u2500\u2500 resources\/\r\n$         \u251c\u2500\u2500 templates\/\r\n$         \u2502   \u251c\u2500\u2500 layout.html\r\n$         \u2502   \u2514\u2500\u2500 authentication\/\r\n$         \u2502   \u2514\u2500\u2500 company\/\r\n$         \u2502   \u2514\u2500\u2500 home\/\r\n$         \u2502   \u2514\u2500\u2500 jobs\/\r\n$         \u2502   \u2514\u2500\u2500 fragments\/\r\n$         \u2502       \u2514\u2500\u2500 forms.html\r\n$         \u2514\u2500\u2500 static\/\r\n$             \u2514\u2500\u2500 css\/<\/code><\/pre>\n<\/div>\n<h3 id=\"job-listing-page\">Job listing page<\/h3>\n<div class=\"clipboard\">\n<pre><code class=\"language-html\">&lt;!DOCTYPE HTML&gt;\r\n&lt;html xmlns:th=&quot;http:\/\/www.thymeleaf.org&quot; xmlns:layout=&quot;http:\/\/www.ultraq.net.nz\/thymeleaf\/layout&quot;\r\n    xmlns:sec=&quot;http:\/\/www.thymeleaf.org\/extras\/spring-security&quot; layout:decorate=&quot;~{layout}&quot;&gt;\r\n\r\n&lt;head&gt;\r\n    &lt;title&gt;[[#{jobPost.list.headline}]]&lt;\/title&gt;\r\n    &lt;style type=&quot;text\/css&quot;&gt;\r\n    &lt;\/style&gt;\r\n&lt;\/head&gt;\r\n\r\n&lt;body&gt;\r\n    &lt;div layout:fragment=&quot;content&quot;&gt;\r\n        &lt;!-- Page Header --&gt;\r\n        &lt;div class=&quot;page-header row mb-4&quot;&gt;\r\n            &lt;div class=&quot;col-md-8&quot;&gt;\r\n                &lt;h1 class=&quot;fw-bold&quot;&gt;Find Your Dream Job&lt;\/h1&gt;\r\n                &lt;p class=&quot;fs-3&quot;&gt;Browse through our latest job openings\r\n                &lt;\/p&gt;\r\n            &lt;\/div&gt;\r\n            &lt;div sec:authorize=&quot;hasRole(&#x27;ADMIN&#x27;)&quot; class=&quot;col-md-4 text-md-end action-buttons mt-3 mt-md-0&quot;&gt;\r\n                &lt;a sec:authorize=&quot;hasRole(&#x27;ADMIN&#x27;)&quot; th:href=&quot;@{\/jobs\/add}&quot;\r\n                    class=&quot;btn btn-create btn-lg text-white me-2&quot;&gt;&lt;i class=&quot;bi bi-plus-circle me-1&quot;&gt;&lt;\/i&gt;\r\n                    [[#{jobPost.list.createNew}]]&lt;\/a&gt;\r\n            &lt;\/div&gt;\r\n        &lt;\/div&gt;\r\n\r\n        &lt;div class=&quot;row&quot;&gt;\r\n            &lt;!-- Filters Sidebar --&gt;\r\n            &lt;div class=&quot;col-lg-3&quot;&gt;\r\n                &lt;form th:action=&quot;@{\/jobs}&quot; method=&quot;get&quot;&gt;\r\n                    &lt;div class=&quot;filter-card card p-3 mb-4&quot;&gt;\r\n                        &lt;div class=&quot;input-group&quot;&gt;\r\n                            &lt;span class=&quot;input-group-text bg-transparent border-0&quot;&gt;\r\n                                &lt;i class=&quot;bi bi-search&quot;&gt;&lt;\/i&gt;\r\n                            &lt;\/span&gt;\r\n                            &lt;input type=&quot;text&quot; name=&quot;searchSkill&quot; th:value=&quot;${searchSkill}&quot;\r\n                                class=&quot;form-control border-0 bg-transparent&quot; placeholder=&quot;Skill...&quot;&gt;\r\n                            &lt;button class=&quot;btn btn-primary&quot;&gt;Search&lt;\/button&gt;\r\n                        &lt;\/div&gt;\r\n                    &lt;\/div&gt;\r\n                &lt;\/form&gt;\r\n            &lt;\/div&gt;\r\n\r\n            &lt;!-- Job Listings --&gt;\r\n            &lt;div class=&quot;col-lg-9&quot;&gt;\r\n                &lt;div class=&quot;row row-cols-1 row-cols-md-2 g-4&quot;&gt;\r\n                    &lt;div th:each=&quot;jobPost : ${jobPosts}&quot; class=&quot;col&quot;&gt;\r\n                        &lt;div class=&quot;card job-card h-100&quot;&gt;\r\n                            &lt;div class=&quot;card-body&quot;&gt;\r\n                                &lt;div class=&quot;d-flex justify-content-between align-items-start mb-3&quot;&gt;\r\n                                    &lt;div class=&quot;company-logo&quot;&gt;&lt;img\r\n                                            th:src=&quot;@{https:\/\/ui-avatars.com\/api\/?name={name}(name=${jobPost.company.name})}&quot;\r\n                                            alt=&quot;Company Logo&quot; width=&quot;32&quot; height=&quot;32&quot; class=&quot;rounded-circle me-2&quot;&gt;&lt;\/div&gt;\r\n                                &lt;\/div&gt;\r\n                                &lt;h5 class=&quot;card-title&quot;&gt;\r\n                                    &lt;a th:text=&quot;${jobPost.title}&quot; th:href=&quot;@{\/jobs\/view\/{id}(id=${jobPost.id})}&quot;\r\n                                        class=&quot;job-title-link&quot;&gt;Developer&lt;\/a&gt;\r\n                                &lt;\/h5&gt;\r\n                                &lt;p class=&quot;card-text mb-2&quot;&gt;\r\n                                    &lt;a href=&quot;#&quot; class=&quot;text-decoration-none&quot;&gt;&lt;span\r\n                                            th:text=&quot;${jobPost.company.name}&quot;&gt;jobType&lt;\/span&gt;&lt;\/a&gt;\r\n                                &lt;\/p&gt;\r\n                                &lt;div class=&quot;mb-3&quot;&gt;\r\n                                    &lt;span th:text=&quot;${jobPost.jobType}&quot; class=&quot;job-tag job-type&quot;&gt;jobType&lt;\/span&gt;\r\n                                    &lt;!-- &lt;span class=&quot;job-tag salary&quot;&gt;$7,000 - $9,000&lt;\/span&gt; --&gt;\r\n                                    &lt;span th:text=&quot;${jobPost.workModel}&quot; class=&quot;job-tag work-mode&quot;&gt;Hybrid&lt;\/span&gt;\r\n                                &lt;\/div&gt;\r\n                                &lt;p class=&quot;card-text text-muted small&quot;&gt;\r\n                                    &lt;i th:text=&quot;${jobPost.location}&quot; class=&quot;bi bi-geo-alt me-1&quot;&gt;\r\n                                    &lt;\/i&gt;\r\n                                &lt;\/p&gt;\r\n                                &lt;p th:text=&quot;${#strings.abbreviate(jobPost.description, 150)}&quot; class=&quot;card-text&quot;&gt;&lt;\/p&gt;\r\n                                &lt;div class=&quot;d-flex justify-content-between align-items-center action-buttons&quot;&gt;\r\n                                    &lt;span class=&quot;text-muted small&quot;&gt;&lt;\/span&gt;\r\n                                    &lt;a sec:authorize=&quot;!hasRole(&#x27;ADMIN&#x27;)&quot; th:href=&quot;@{\/jobs\/view\/{id}(id=${jobPost.id})}&quot;\r\n                                        class=&quot;btn btn-lg btn-primary&quot;&gt;View Details&lt;\/a&gt;\r\n                                    &lt;a sec:authorize=&quot;hasRole(&#x27;ADMIN&#x27;)&quot; th:href=&quot;@{\/jobs\/edit\/{id}(id=${jobPost.id})}&quot;\r\n                                        class=&quot;btn btn-lg btn-primary&quot;&gt;&lt;i class=&quot;bi bi-pencil-square&quot;&gt;&lt;\/i&gt; Edit&lt;\/a&gt;\r\n                                &lt;\/div&gt;\r\n                            &lt;\/div&gt;\r\n                        &lt;\/div&gt;\r\n                    &lt;\/div&gt;\r\n                &lt;\/div&gt;\r\n            &lt;\/div&gt;\r\n        &lt;\/div&gt;\r\n        &lt;script src=&quot;https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jquery\/3.7.1\/jquery.min.js&quot;&gt;&lt;\/script&gt;\r\n    &lt;\/div&gt;\r\n&lt;\/body&gt;\r\n\r\n&lt;\/html&gt;<\/code><\/pre>\n<\/div>\n<ul>\n<li>The <code>Create New Job<\/code> button is only available for ADMIN.<\/li>\n<li>We provide a search form to let users filter jobs by skill.<\/li>\n<li>We use <code>th:each<\/code> attribute to iterate over a list of jobs. Each job post is shown as a card. The job description was abbreviated to 150 characters. Edit button only for ADMIN. The title is a clickable link to the job details.<\/li>\n<li>For the styling we uses Bootstrap 5 and custom CSS.<\/li>\n<\/ul>\n<p>\nHere is what it looks like as an admin:\n<\/p>\n<p><a href=\"\/wp-content\/uploads\/2026\/06\/job-listing.png\"><img decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/06\/job-listing.png\" alt=\"\" width=\"1607\" height=\"860\" class=\"aligncenter size-full wp-image-55366\" srcset=\"\/wp-content\/uploads\/2026\/06\/job-listing.png 1607w, \/wp-content\/uploads\/2026\/06\/job-listing-300x161.png 300w, \/wp-content\/uploads\/2026\/06\/job-listing-1024x548.png 1024w, \/wp-content\/uploads\/2026\/06\/job-listing-768x411.png 768w, \/wp-content\/uploads\/2026\/06\/job-listing-1536x822.png 1536w, \/wp-content\/uploads\/2026\/06\/job-listing-600x321.png 600w\" sizes=\"(max-width: 1607px) 100vw, 1607px\" \/><\/a><\/p>\n<h2 id=\"spring-ai-integration\">Spring AI Integration<\/h2>\n<p>\nNext, we introduce an intelligent feature that automates the process of adding relevant skills to a job post.\n<\/p>\n<p>\nWe will leverage <em>Spring AI<\/em>, a powerful library that simplifies communication between our Spring Boot application and advanced AI models from providers like OpenAI. The process is straightforward: when an admin is creating or editing a job post, they can click a &#8220;Generate Skills&#8221; button. Behind the scenes, our application prepares relevant data before sending it to the OpenAI model and consumes the output.\n<\/p>\n<p>\nThe most important part, we want to turn the AI-generated response into structured data like a Java record.\n<\/p>\n<div class=\"clipboard\">\n<pre><code class=\"language-java\">private List&lt;SkillTagDTO&gt; generateSkills(JobPostDTO jobPostDTO, List&lt;SkillTagDTO&gt; skillTags)\r\n            throws JsonProcessingException, JsonMappingException {\r\n    ObjectMapper objectMapper = new ObjectMapper();\r\n    String skillCatalogJson = objectMapper.writeValueAsString(skillTags);\r\n    BeanOutputConverter&lt;SkillResponse&gt; outputConverter =\r\n                new BeanOutputConverter&lt;&gt;(new ParameterizedTypeReference&lt;SkillResponse&gt;() {});\r\n    String format = outputConverter.getFormat();\r\n    \/\/ @formatter:off\r\n    String promptStr =\r\n            &quot;&quot;&quot;\r\n            You are an AI assistant that extracts required skills from a job description.\r\n            TASK:\r\n            - Only return skills present in the provided JSON skill catalog.\r\n            - Matching is case-insensitive.\r\n            - Do not invent or include skills not in the catalog.\r\n            - Output strictly as a JSON array of objects.\r\n\r\n            NOW PROCESS:\r\n            &lt;JOB_DESCRIPTION&gt;\r\n            {jobDescription}\r\n            &lt;\/JOB_DESCRIPTION&gt;\r\n            &lt;SKILL_LIST&gt;\r\n            {skillCatalog}\r\n            &lt;\/SKILL_LIST&gt;\r\n\r\n            {format}\r\n        &quot;&quot;&quot;;\r\n    \/\/ @formatter:on\r\n    Prompt prompt =\r\n            PromptTemplate.builder()\r\n                    .template(promptStr)\r\n                    .build()\r\n                    .create(\r\n                            Map.of(\r\n                                    &quot;jobDescription&quot;,jobPostDTO.getDescription(),\r\n                                    &quot;skillCatalog&quot;,skillCatalogJson,\r\n                                    &quot;format&quot;,format),\r\n                            OpenAiChatOptions.builder()\r\n                                    .responseFormat(new ResponseFormat(ResponseFormat.Type.JSON_OBJECT, null))\r\n                                    .build());\r\n    var generation = this.chatModel.call(prompt).getResult();\r\n    String outputText = generation.getOutput().getText();\r\n    SkillResponse skillResponse = outputConverter.convert(outputText);\r\n    return skillResponse.skills();\r\n}\r\n\r\nrecord SkillResponse(List&lt;SkillTagDTO&gt; skills) {}<\/code><\/pre>\n<\/div>\n<ul>\n<li>We use the low-level <code>ChatModel<\/code> API directly.<\/li>\n<li>Use Jackson&#8217;s <code>ObjectMapper<\/code> to convert the list of <code>SkillTagDTO<\/code> objects into JSON string. We want the AI Model to give us skills that only present in our provided skill catalog.<\/li>\n<li><code>BeanOutputConverter<\/code>: to generate a JSON schema based on a given Java class, which is then used to transform the LLM output into our desired type <code>SkillResponse<\/code> record.<\/li>\n<li>We build the prompt using a template and inject the job description, skill catalog (as JSON), and the output format. We configure the OpenAI Chat API to respond with a JSON object through <code>OpenAiChatOptions<\/code>.<\/li>\n<li>Send the prompt to the OpenAI chat model using its <code>call<\/code> method.<\/li>\n<li>Gets the AI&#8217;s output text, use <code>outputConverter<\/code> to parse the JSON response into <code>SkillResponse<\/code> object. Return the list of extracted skills.<\/li>\n<\/ul>\n<p><em>Demo showing generate skills from job description using Spring AI and OpenAI.<\/em><\/p>\n<p><a href=\"\/wp-content\/uploads\/2026\/06\/editjob_generateskills.gif\"><img decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/06\/editjob_generateskills.gif\" alt=\"\" width=\"1920\" height=\"1080\" class=\"aligncenter size-full wp-image-55365\" \/><\/a><\/p>\n<h2 id=\"running-the-project-end-to-end-execution\">Running the Project: End-to-End Execution<\/h2>\n<div class=\"clipboard\">\n<pre><code class=\"language-sh\">$ export OPENAI_API_KEY=your_key<\/code><\/pre>\n<\/div>\n<p>\nBuild the project and run the application in development mode.\n<\/p>\n<div class=\"clipboard\">\n<pre><code class=\"language-sh\">$ mvn clean package\r\n\r\n$ mvn spring-boot:run<\/code><\/pre>\n<\/div>\n<p>\nOpen <a href=\"http:\/\/localhost:8080\" target=\"_blank\">localhost.<\/a>\n<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>\nIn this tutorial, we&#8217;ve successfully built a fully functional job board web application that demonstrate the power of modern Java development. We&#8217;ve gained hands-on experience integrating <em>Spring Boot, Thymeleaf, and GridDB Cloud<\/em> to create a complete full-stack solution from the ground up.\n<\/p>\n<p>\nOne of the most exciting aspects of our implementation was integrating <em>Spring AI&#8217;s<\/em> support for <em>OpenAI&#8217;s<\/em> Structured Outputs. This feature transforms how web handle AI-generated content by ensuring predictable, well-formatted responses.\n<\/p>\n<p>\nWhile this application provides a solid foundation for any job board platform, there are numerous features we can consider adding:\n<\/p>\n<ul>\n<li>A complete user authentication and registration.<\/li>\n<li>Allowing recruiters to register and post jobs under their own account.<\/li>\n<li>Introduce pagination to display jobs.<\/li>\n<li>Tracing the request and response of the LLM call into the observability platform.<\/li>\n<li>Add cache in generating skills. If the job description hasn&#8217;t changed, then we should not call the OpenAI API.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>In this tutorial, we&#8217;ll build a fully functional job board web application from the ground up. Our application will allow users to browse available positions, search for jobs based on specific skills, and administrators can manage job listings. We&#8217;ll be working with three powerful technologies: Spring Boot to handle our backend component, Thymeleaf for creating [&hellip;]<\/p>\n","protected":false},"author":41,"featured_media":55363,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[121],"tags":[],"class_list":["post-55362","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.1.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Building a Modern Job Board with Spring Boot &amp; GridDB Cloud | GridDB: Open Source Time Series Database for IoT<\/title>\n<meta name=\"description\" content=\"In this tutorial, we&#039;ll build a fully functional job board web application from the ground up. Our application will allow users to browse available\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Building a Modern Job Board with Spring Boot &amp; GridDB Cloud | GridDB: Open Source Time Series Database for IoT\" \/>\n<meta property=\"og:description\" content=\"In this tutorial, we&#039;ll build a fully functional job board web application from the ground up. Our application will allow users to browse available\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/\" \/>\n<meta property=\"og:site_name\" content=\"GridDB: Open Source Time Series Database for IoT\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/griddbcommunity\/\" \/>\n<meta property=\"article:published_time\" content=\"2026-06-10T21:29:25+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-17T21:29:47+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.griddb.net\/wp-content\/uploads\/2026\/06\/cover-jobboard.png\" \/>\n\t<meta property=\"og:image:width\" content=\"663\" \/>\n\t<meta property=\"og:image:height\" content=\"498\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"griddb-admin\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@GridDBCommunity\" \/>\n<meta name=\"twitter:site\" content=\"@GridDBCommunity\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"griddb-admin\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"9 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/\"},\"author\":{\"name\":\"griddb-admin\",\"@id\":\"https:\/\/www.griddb.net\/en\/#\/schema\/person\/4fe914ca9576878e82f5e8dd3ba52233\"},\"headline\":\"Building a Modern Job Board with Spring Boot &#038; GridDB Cloud\",\"datePublished\":\"2026-06-10T21:29:25+00:00\",\"dateModified\":\"2026-06-17T21:29:47+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/\"},\"wordCount\":1723,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/www.griddb.net\/en\/#organization\"},\"image\":{\"@id\":\"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/#primaryimage\"},\"thumbnailUrl\":\"\/wp-content\/uploads\/2026\/06\/cover-jobboard.png\",\"articleSection\":[\"Blog\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/\",\"url\":\"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/\",\"name\":\"Building a Modern Job Board with Spring Boot & GridDB Cloud | GridDB: Open Source Time Series Database for IoT\",\"isPartOf\":{\"@id\":\"https:\/\/www.griddb.net\/en\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/#primaryimage\"},\"thumbnailUrl\":\"\/wp-content\/uploads\/2026\/06\/cover-jobboard.png\",\"datePublished\":\"2026-06-10T21:29:25+00:00\",\"dateModified\":\"2026-06-17T21:29:47+00:00\",\"description\":\"In this tutorial, we'll build a fully functional job board web application from the ground up. Our application will allow users to browse available\",\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/#primaryimage\",\"url\":\"\/wp-content\/uploads\/2026\/06\/cover-jobboard.png\",\"contentUrl\":\"\/wp-content\/uploads\/2026\/06\/cover-jobboard.png\",\"width\":663,\"height\":498},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.griddb.net\/en\/#website\",\"url\":\"https:\/\/www.griddb.net\/en\/\",\"name\":\"GridDB: Open Source Time Series Database for IoT\",\"description\":\"GridDB is an open source time-series database with the performance of NoSQL and convenience of SQL\",\"publisher\":{\"@id\":\"https:\/\/www.griddb.net\/en\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.griddb.net\/en\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/www.griddb.net\/en\/#organization\",\"name\":\"Fixstars\",\"url\":\"https:\/\/www.griddb.net\/en\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.griddb.net\/en\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/griddb.net\/wp-content\/uploads\/2019\/04\/fixstars_logo_web_tagline.png\",\"contentUrl\":\"https:\/\/griddb.net\/wp-content\/uploads\/2019\/04\/fixstars_logo_web_tagline.png\",\"width\":200,\"height\":83,\"caption\":\"Fixstars\"},\"image\":{\"@id\":\"https:\/\/www.griddb.net\/en\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/www.facebook.com\/griddbcommunity\/\",\"https:\/\/x.com\/GridDBCommunity\",\"https:\/\/www.linkedin.com\/company\/griddb-by-toshiba\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.griddb.net\/en\/#\/schema\/person\/4fe914ca9576878e82f5e8dd3ba52233\",\"name\":\"griddb-admin\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.griddb.net\/en\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/5bceca1cafc06886a7ba873e2f0a28011a1176c4dea59709f735b63ae30d0342?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/5bceca1cafc06886a7ba873e2f0a28011a1176c4dea59709f735b63ae30d0342?s=96&d=mm&r=g\",\"caption\":\"griddb-admin\"},\"url\":\"https:\/\/www.griddb.net\/en\/author\/griddb-admin\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Building a Modern Job Board with Spring Boot & GridDB Cloud | GridDB: Open Source Time Series Database for IoT","description":"In this tutorial, we'll build a fully functional job board web application from the ground up. Our application will allow users to browse available","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/","og_locale":"en_US","og_type":"article","og_title":"Building a Modern Job Board with Spring Boot & GridDB Cloud | GridDB: Open Source Time Series Database for IoT","og_description":"In this tutorial, we'll build a fully functional job board web application from the ground up. Our application will allow users to browse available","og_url":"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/","og_site_name":"GridDB: Open Source Time Series Database for IoT","article_publisher":"https:\/\/www.facebook.com\/griddbcommunity\/","article_published_time":"2026-06-10T21:29:25+00:00","article_modified_time":"2026-06-17T21:29:47+00:00","og_image":[{"width":663,"height":498,"url":"https:\/\/www.griddb.net\/wp-content\/uploads\/2026\/06\/cover-jobboard.png","type":"image\/png"}],"author":"griddb-admin","twitter_card":"summary_large_image","twitter_creator":"@GridDBCommunity","twitter_site":"@GridDBCommunity","twitter_misc":{"Written by":"griddb-admin","Est. reading time":"9 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/#article","isPartOf":{"@id":"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/"},"author":{"name":"griddb-admin","@id":"https:\/\/www.griddb.net\/en\/#\/schema\/person\/4fe914ca9576878e82f5e8dd3ba52233"},"headline":"Building a Modern Job Board with Spring Boot &#038; GridDB Cloud","datePublished":"2026-06-10T21:29:25+00:00","dateModified":"2026-06-17T21:29:47+00:00","mainEntityOfPage":{"@id":"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/"},"wordCount":1723,"commentCount":0,"publisher":{"@id":"https:\/\/www.griddb.net\/en\/#organization"},"image":{"@id":"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/#primaryimage"},"thumbnailUrl":"\/wp-content\/uploads\/2026\/06\/cover-jobboard.png","articleSection":["Blog"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/","url":"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/","name":"Building a Modern Job Board with Spring Boot & GridDB Cloud | GridDB: Open Source Time Series Database for IoT","isPartOf":{"@id":"https:\/\/www.griddb.net\/en\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/#primaryimage"},"image":{"@id":"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/#primaryimage"},"thumbnailUrl":"\/wp-content\/uploads\/2026\/06\/cover-jobboard.png","datePublished":"2026-06-10T21:29:25+00:00","dateModified":"2026-06-17T21:29:47+00:00","description":"In this tutorial, we'll build a fully functional job board web application from the ground up. Our application will allow users to browse available","inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.griddb.net\/en\/blog\/building-a-modern-job-board-with-spring-boot-griddb-cloud\/#primaryimage","url":"\/wp-content\/uploads\/2026\/06\/cover-jobboard.png","contentUrl":"\/wp-content\/uploads\/2026\/06\/cover-jobboard.png","width":663,"height":498},{"@type":"WebSite","@id":"https:\/\/www.griddb.net\/en\/#website","url":"https:\/\/www.griddb.net\/en\/","name":"GridDB: Open Source Time Series Database for IoT","description":"GridDB is an open source time-series database with the performance of NoSQL and convenience of SQL","publisher":{"@id":"https:\/\/www.griddb.net\/en\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.griddb.net\/en\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/www.griddb.net\/en\/#organization","name":"Fixstars","url":"https:\/\/www.griddb.net\/en\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.griddb.net\/en\/#\/schema\/logo\/image\/","url":"https:\/\/griddb.net\/wp-content\/uploads\/2019\/04\/fixstars_logo_web_tagline.png","contentUrl":"https:\/\/griddb.net\/wp-content\/uploads\/2019\/04\/fixstars_logo_web_tagline.png","width":200,"height":83,"caption":"Fixstars"},"image":{"@id":"https:\/\/www.griddb.net\/en\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/griddbcommunity\/","https:\/\/x.com\/GridDBCommunity","https:\/\/www.linkedin.com\/company\/griddb-by-toshiba"]},{"@type":"Person","@id":"https:\/\/www.griddb.net\/en\/#\/schema\/person\/4fe914ca9576878e82f5e8dd3ba52233","name":"griddb-admin","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.griddb.net\/en\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/5bceca1cafc06886a7ba873e2f0a28011a1176c4dea59709f735b63ae30d0342?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/5bceca1cafc06886a7ba873e2f0a28011a1176c4dea59709f735b63ae30d0342?s=96&d=mm&r=g","caption":"griddb-admin"},"url":"https:\/\/www.griddb.net\/en\/author\/griddb-admin\/"}]}},"_links":{"self":[{"href":"https:\/\/www.griddb.net\/en\/wp-json\/wp\/v2\/posts\/55362","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.griddb.net\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.griddb.net\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.griddb.net\/en\/wp-json\/wp\/v2\/users\/41"}],"replies":[{"embeddable":true,"href":"https:\/\/www.griddb.net\/en\/wp-json\/wp\/v2\/comments?post=55362"}],"version-history":[{"count":3,"href":"https:\/\/www.griddb.net\/en\/wp-json\/wp\/v2\/posts\/55362\/revisions"}],"predecessor-version":[{"id":55369,"href":"https:\/\/www.griddb.net\/en\/wp-json\/wp\/v2\/posts\/55362\/revisions\/55369"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.griddb.net\/en\/wp-json\/wp\/v2\/media\/55363"}],"wp:attachment":[{"href":"https:\/\/www.griddb.net\/en\/wp-json\/wp\/v2\/media?parent=55362"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.griddb.net\/en\/wp-json\/wp\/v2\/categories?post=55362"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.griddb.net\/en\/wp-json\/wp\/v2\/tags?post=55362"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}