Thị trường việc làm IT 2026: kỹ năng nào dev nên đầu tư
1. Tổng quan
Thị trường việc làm IT đang thay đổi theo hướng đòi hỏi nhiều hơn ở mỗi developer. Viết được tính năng là chưa đủ. Bạn còn cần hiểu tính năng đó chạy, lỗi và tốn chi phí ra sao trên production.
Bài này không đưa số liệu hay dự đoán lương. Thay vào đó, bài phân tích năm nhóm kỹ năng nền tảng đang được quan tâm, giải thích lý do, và minh hoạ mỗi nhóm bằng code Java 21 / Spring Boot 3.x. Đọc xong, bạn sẽ biết nên ưu tiên học gì, áp dụng ngay vào dự án thế nào, và khi nào không nên chạy theo xu hướng.
2. Vì sao yêu cầu kỹ năng đang dịch chuyển
Có ba lực đẩy chính, và bạn có thể tự quan sát chúng trong hầu hết các tin tuyển dụng:
- Công cụ AI hỗ trợ viết code đã phổ biến. Phần code “boilerplate” được sinh nhanh hơn. Giá trị của dev chuyển sang thiết kế, review, kiểm thử và chịu trách nhiệm về kết quả.
- Doanh nghiệp chú trọng chi phí vận hành. Một service dùng ít tài nguyên hơn và dễ chẩn đoán lỗi hơn sẽ trực tiếp tiết kiệm tiền.
- Team nhỏ hơn, ownership rộng hơn. Mô hình “you build it, you run it” khiến dev phải biết cả đóng gói, giám sát và xử lý sự cố.
Từ đó, năm nhóm kỹ năng dưới đây nổi lên như những khoản đầu tư có lợi nhuận lâu dài.
3. Tích hợp AI vào ứng dụng một cách có kiểm soát
Kỹ năng được nhắc nhiều nhất hiện nay không phải là “biết dùng chatbot”. Đó là khả năng đưa LLM vào hệ thống thật mà vẫn giữ được tính ổn định, kiểm thử được và có phương án dự phòng.
Nguyên tắc cốt lõi: coi LLM như một dependency bên ngoài không đáng tin. Output của nó có thể sai định dạng, chậm, hoặc lỗi mạng.
3.1. Tách LLM sau một interface
@FunctionalInterface
public interface LlmClient {
String complete(String prompt);
}
public enum TicketCategory { BILLING, TECHNICAL, OTHER }
public class TicketClassifier {
private final LlmClient llm;
public TicketClassifier(LlmClient llm) {
this.llm = llm;
}
public TicketCategory classify(String message) {
String prompt = """
Phân loại yêu cầu hỗ trợ sau vào đúng một nhãn: BILLING, TECHNICAL, OTHER.
Chỉ trả về nhãn, không giải thích.
Yêu cầu: %s
""".formatted(message);
try {
String raw = llm.complete(prompt).trim().toUpperCase();
return TicketCategory.valueOf(raw);
} catch (RuntimeException e) {
return TicketCategory.OTHER;
}
}
}
Giải thích:
LlmClientlà interface của riêng bạn. Implementation thật có thể gọi bất kỳ nhà cung cấp nào qua HTTP hoặc qua một thư viện như Spring AI. Business logic không phụ thuộc vào nhà cung cấp cụ thể.- Prompt yêu cầu output là một nhãn duy nhất. Ràng buộc định dạng giúp bạn parse được kết quả.
TicketCategory.valueOf(raw)vừa parse vừa validate. Nếu model trả về câu dài dòng,IllegalArgumentExceptionbị bắt và hệ thống fallback vềOTHER. Lỗi mạng (thường làRuntimeException) cũng đi vào nhánh này.
3.2. Kiểm thử mà không cần gọi model thật
Vì LlmClient là functional interface, bạn có thể truyền lambda làm fake:
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
class TicketClassifierTest {
@Test
void givenValidLabel_whenClassify_thenReturnCategory() {
TicketClassifier classifier = new TicketClassifier(prompt -> " billing\n");
assertThat(classifier.classify("Tôi bị trừ tiền hai lần"))
.isEqualTo(TicketCategory.BILLING);
}
@Test
void givenUnexpectedOutput_whenClassify_thenFallbackToOther() {
TicketClassifier classifier = new TicketClassifier(prompt -> "Có lẽ là BILLING");
assertThat(classifier.classify("Tôi bị trừ tiền hai lần"))
.isEqualTo(TicketCategory.OTHER);
}
@Test
void givenClientFailure_whenClassify_thenFallbackToOther() {
TicketClassifier classifier = new TicketClassifier(prompt -> {
throw new IllegalStateException("timeout");
});
assertThat(classifier.classify("App bị crash")).isEqualTo(TicketCategory.OTHER);
}
}
Test thứ nhất cho thấy code chịu được khoảng trắng và chữ thường. Hai test sau chứng minh hệ thống không sập khi model “nói nhiều” hoặc khi kết nối lỗi. Đây chính là thứ nhà tuyển dụng muốn thấy: bạn biết LLM sẽ sai và đã thiết kế cho trường hợp đó.
Lưu ý: Không nên dùng LLM cho những việc mà một rule, một regex hay một câu SQL làm được chính xác và rẻ hơn. Cũng tránh dùng LLM cho quyết định cần kết quả tất định và có thể kiểm toán, ví dụ tính tiền hoặc phân quyền.
4. Concurrency hiện đại với virtual threads
Phần lớn backend là I/O-bound: gọi database, gọi API khác. Java 21 đưa virtual threads thành tính năng chính thức, cho phép viết code blocking quen thuộc mà vẫn phục vụ được rất nhiều request đồng thời. Hiểu khi nào nên và không nên dùng là một điểm cộng rõ rệt trong phỏng vấn.
import java.time.Duration;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class PriceAggregator {
public static void main(String[] args) throws Exception {
List<String> suppliers = List.of("SupplierA", "SupplierB", "SupplierC");
long start = System.currentTimeMillis();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = suppliers.stream()
.map(s -> executor.submit(() -> fetchPrice(s)))
.toList();
for (Future<String> f : futures) {
System.out.println(f.get());
}
}
System.out.println("Tổng thời gian: " + (System.currentTimeMillis() - start) + " ms");
}
static String fetchPrice(String supplier) throws InterruptedException {
Thread.sleep(Duration.ofSeconds(1)); // giả lập gọi API chậm
return supplier + " -> 100000 VND, virtual=" + Thread.currentThread().isVirtual();
}
}
newVirtualThreadPerTaskExecutor()tạo một virtual thread cho mỗi task. Bạn không cần tinh chỉnh kích thước pool.- Khối
try-with-resourcesđảm bảo executor chờ mọi task xong rồi mới đóng. - Ba lời gọi chậm một giây chạy song song, nên tổng thời gian xấp xỉ một giây:
SupplierA -> 100000 VND, virtual=true
SupplierB -> 100000 VND, virtual=true
SupplierC -> 100000 VND, virtual=true
Tổng thời gian: 1015 ms
Trong Spring Boot 3.2 trở lên, bạn bật virtual threads cho request handling bằng một dòng:
spring.threads.virtual.enabled=true
Lưu ý: Virtual threads không làm code CPU-bound nhanh hơn. Hãy cẩn thận với các khối
synchronizedbao quanh thao tác I/O dài, vì chúng có thể ghim virtual thread vào carrier thread. Mức độ ảnh hưởng còn tuỳ phiên bản JDK. Ngoài ra, chạy nhiều task đồng thời hơn có thể làm cạn connection pool của database. Bạn vẫn cần giới hạn tài nguyên ở tầng phía sau.
5. Observability: biết hệ thống đang làm gì
Khi bạn chịu trách nhiệm cả phần vận hành, câu hỏi “tại sao chậm?” phải trả lời được bằng dữ liệu. Metrics, logs và traces là ba trụ cột. Trong hệ sinh thái Spring, Micrometer và Actuator là điểm khởi đầu tự nhiên.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Thêm một business metric tự định nghĩa:
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final Counter placedOrders;
public OrderService(MeterRegistry registry) {
this.placedOrders = Counter.builder("orders.placed")
.description("Số đơn hàng đặt thành công")
.tag("channel", "web")
.register(registry);
}
public void placeOrder(String orderId) {
// ... lưu đơn hàng, gọi PaymentService ...
placedOrders.increment();
}
}
management.endpoints.web.exposure.include=health,metrics
MeterRegistryđược Spring Boot tự cấu hình và inject vào bean.- Tag
channelcho phép tách số liệu theo kênh bán hàng khi xem dashboard. - Dòng cấu hình chỉ mở hai endpoint
healthvàmetrics, không mở tất cả.
Sau vài đơn hàng, gọi GET /actuator/metrics/orders.placed:
{
"name": "orders.placed",
"description": "Số đơn hàng đặt thành công",
"measurements": [ { "statistic": "COUNT", "value": 3.0 } ],
"availableTags": [ { "tag": "channel", "values": [ "web" ] } ]
}
Lưu ý: Không dùng giá trị có số lượng lớn và không giới hạn (như
orderIdhaycustomerId) làm tag. Mỗi giá trị tag tạo ra một time series mới, nên hệ thống metrics sẽ phình to rất nhanh. Cũng đừng expose toàn bộ actuator endpoint ra Internet.
6. Testing với hạ tầng thật bằng Testcontainers
Khi AI có thể sinh code nhanh, test chính là thứ chứng minh code đúng. Integration test chạy trên database thật, thay vì một database in-memory khác dialect, giúp bắt lỗi mà mock không bắt được.
Các dependency sau được Spring Boot quản lý version:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
Entity và repository tối giản:
@Entity
public class Customer {
@Id @GeneratedValue
private Long id;
private String email;
protected Customer() {}
public Customer(String email) { this.email = email; }
public Long getId() { return id; }
public String getEmail() { return email; }
}
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByEmail(String email);
}
Integration test:
@SpringBootTest
@Testcontainers
class CustomerRepositoryIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired
CustomerRepository repository;
@Test
void givenSavedCustomer_whenFindByEmail_thenReturnCustomer() {
repository.save(new Customer("lan@example.com"));
assertThat(repository.findByEmail("lan@example.com"))
.isPresent()
.get()
.extracting(Customer::getEmail)
.isEqualTo("lan@example.com");
}
}
@Testcontainersvà@Containerkhởi động một PostgreSQL thật trong Docker trước khi test chạy.@ServiceConnection(có từ Spring Boot 3.1) tự đưa URL, username và password của container vàoDataSource. Bạn không cần@DynamicPropertySource.- Query
findByEmailđược Spring Data sinh ra và chạy trên đúng loại database của production.
Lưu ý: Testcontainers cần Docker trên máy dev và trên CI. Test cũng chậm hơn unit test. Hãy giữ phần lớn logic ở unit test thuần, chỉ dùng Testcontainers cho tầng tích hợp như repository, messaging và migration.
7. Đóng gói và vận hành container
Hiểu cách ứng dụng được đóng gói và chạy là kỹ năng tối thiểu khi làm việc với Kubernetes hay các nền tảng cloud. Một Dockerfile multi-stage gọn gàng cho thấy bạn quan tâm đến kích thước image và bảo mật:
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw -q package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
USER 1000
ENTRYPOINT ["java", "-jar", "app.jar"]
- Stage
buildcó JDK và Maven wrapper để biên dịch. - Stage cuối chỉ chứa JRE và file jar, nên image nhỏ hơn và có ít bề mặt tấn công hơn.
USER 1000giúp container không chạy bằng root, một lỗi rất hay gặp.
Bạn có thể kết hợp với endpoint /actuator/health ở mục 5 làm liveness/readiness probe khi deploy.
Lưu ý: Nếu team đã dùng Buildpacks (ví dụ qua
./mvnw spring-boot:build-image), bạn không cần tự viết Dockerfile. Quan trọng là hiểu image chứa gì, chạy bằng user nào, và giới hạn bộ nhớ ảnh hưởng tới JVM ra sao.
8. Khi nào không nên chạy theo xu hướng
Đầu tư kỹ năng cũng cần tỉnh táo:
- Đừng bỏ qua nền tảng. Cấu trúc dữ liệu, SQL, HTTP, transaction, xử lý lỗi vẫn là thứ được hỏi trong mọi buổi phỏng vấn. AI và framework thay đổi nhanh hơn nhiều so với các kiến thức này.
- Đừng học dàn trải. Chọn một hai kỹ năng gắn với công việc hiện tại, áp dụng vào dự án thật, rồi mới mở rộng. Một PR thêm metrics và integration test có giá trị hơn mười chứng chỉ chưa dùng tới.
- Đừng đưa công nghệ vào chỉ để có trong CV. Một hệ thống nhỏ, ít người dùng không cần Kubernetes, microservices hay LLM. Việc biết nói “không cần” kèm lý do cũng là một kỹ năng được đánh giá cao.
- Đừng giao phó hoàn toàn cho công cụ sinh code. Bạn vẫn là người chịu trách nhiệm. Hãy đọc, hiểu và viết test cho mọi đoạn code được sinh ra.
9. Kết luận
Thị trường IT đang thưởng cho những developer làm chủ được trọn vòng đời tính năng: thiết kế, viết code, kiểm thử, đóng gói và vận hành. Bài viết đã đi qua năm nhóm kỹ năng đáng đầu tư:
- Tích hợp LLM sau một interface, có validate và fallback.
- Dùng virtual threads cho workload I/O-bound.
- Thêm observability bằng Micrometer và Actuator.
- Viết integration test trên hạ tầng thật với Testcontainers.
- Đóng gói container an toàn.
Mỗi kỹ năng đều có giới hạn riêng, và biết giới hạn đó quan trọng không kém biết cách dùng. Bạn hãy chọn một mục, áp dụng ngay vào service đang làm trong tuần này. Đó là cách nhanh nhất để biến xu hướng thành kinh nghiệm thật.