As a PHP developer with Symfony experience, Java and Spring Boot often evoke mixed feelings. The reputation: complicated, slow to set up, lots of boilerplate. The reality with Spring Boot is different. The concepts are familiar — it is essentially Symfony, just in Java.
Concept Mapping: Symfony ↔ Spring Boot
| Symfony | Spring Boot | Description |
|---|---|---|
services.yaml |
@Component, @Service |
Service registration |
#[Route] |
@RequestMapping, @GetMapping |
Routing |
AbstractController |
@RestController |
Controller base |
| Doctrine ORM | Spring Data JPA / Hibernate | ORM |
| Repository interface | JpaRepository<Entity, ID> |
Database access |
EventSubscriber |
@EventListener |
Events |
.env |
application.properties |
Configuration |
| Composer | Maven / Gradle | Dependency management |
bin/console |
./mvnw, ./gradlew |
CLI |
Project Setup with Spring Initializr
The equivalent of composer create-project symfony/skeleton is start.spring.io. There you select:
- Project: Maven or Gradle
- Language: Java
- Spring Boot Version: 3.x (current)
- Dependencies: Spring Web, Spring Data JPA, H2 Database (for development)
The generated project has the following structure:
my-api/
├── src/main/java/com/example/myapi/
│ ├── MyApiApplication.java # Entry point (like public/index.php)
│ ├── controller/
│ ├── service/
│ ├── repository/
│ └── entity/
├── src/main/resources/
│ └── application.properties # Configuration (like .env)
├── src/test/java/ # Tests
└── pom.xml # Composer equivalent
Dependency Injection — Just Like Symfony
Symfony and Spring Boot both use dependency injection as a core concept. In Symfony you write:
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Service\ProductService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class ProductController extends AbstractController
{
public function __construct(
private readonly ProductService $productService
) {}
}
In Spring Boot it is almost identical:
package com.example.myapi.controller;
import com.example.myapi.service.ProductService;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProductController {
private final ProductService productService;
// Constructor Injection — analogous to Symfony
public ProductController(ProductService productService) {
this.productService = productService;
}
}
Spring automatically detects classes annotated with @Service, @Component, and @Repository and injects them. Exactly like Symfony service autowiring.
REST Controller
Symfony:
<?php
declare(strict_types=1);
namespace App\Controller\Api;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
#[Route('/api/products', name: 'api_products_')]
class ProductController extends AbstractController
{
#[Route('', name: 'list', methods: ['GET'])]
public function list(): JsonResponse
{
return $this->json(['products' => []]);
}
#[Route('/{id}', name: 'show', methods: ['GET'])]
public function show(int $id): JsonResponse
{
return $this->json(['id' => $id]);
}
}
Spring Boot:
package com.example.myapi.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public List<Product> list() {
return productService.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<Product> show(@PathVariable Long id) {
return productService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Product> create(@RequestBody @Valid ProductDto dto) {
Product created = productService.create(dto);
return ResponseEntity.status(201).body(created);
}
}
Entity and Repository (Analogous to Doctrine)
In Symfony you define a Doctrine entity:
<?php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'products')]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private string $name;
}
In Spring Boot (JPA/Hibernate):
package com.example.myapi.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 255)
private String name;
// Getters and setters
public Long getId() { return id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
Repository
Symfony with Doctrine:
class ProductRepository extends ServiceEntityRepository
{
public function findByCategory(string $category): array;
}
Spring Boot with JPA:
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Method automatically generated based on the name!
List<Product> findByCategory(String category);
}
Spring Data JPA automatically generates the query from the method name — similar to Doctrine's QueryBuilder, but even more magical.
application.properties — The .env Equivalent
# Database connection (analogous to DATABASE_URL)
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=myuser
spring.datasource.password=mypassword
# JPA
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
# Server port
server.port=8080
# Logging
logging.level.com.example=DEBUG
Tests — JUnit Instead of PHPUnit
package com.example.myapi.controller;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void listProductsReturnsEmptyArray() throws Exception {
mockMvc.perform(get("/api/products"))
.andExpect(status().isOk())
.andExpect(content().contentType("application/json"))
.andExpect(jsonPath("$").isArray());
}
}
Conclusion
Spring Boot is surprisingly accessible for Symfony developers. Dependency injection, annotations instead of XML, an ORM with the repository pattern, and convention-based configuration — these are all familiar concepts. The main differences lie in static typing (which actually helps), the build process (Maven/Gradle instead of Composer), and deployment (a JAR file instead of PHP files on a web server).
Comments
Comments are provided by Remark42. By loading comments, data is transmitted to our comment server.